diff --git a/.gitignore b/.gitignore index 898483b2..a20ace75 100644 --- a/.gitignore +++ b/.gitignore @@ -183,3 +183,9 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Data binary +*.dat + +# TorchScript model +*.pt diff --git a/examples/2_ResNet18/README.md b/examples/2_ResNet18/README.md index 2fb756b4..fe530c34 100644 --- a/examples/2_ResNet18/README.md +++ b/examples/2_ResNet18/README.md @@ -1,6 +1,8 @@ # Example 2 - ResNet-18 -This example provides a simple but complete demonstration of how to use the library. +This example provides a more realistic demonstration of how to use the library, using ResNet-18 to classify an image. + +As the input to this model is four-dimensional (batch size, colour, x, y), care must be taken dealing with the data array in Python and Fortran. See [When to transpose arrays](#when-to-transpose-arrays) for more details. ## Description @@ -34,10 +36,22 @@ pip install -r requirements.txt ``` You can check that everything is working by running `resnet18.py`: + ``` python3 resnet18.py ``` -it should produce the result `tensor([[623, 499, 596, 111, 813]])`. + +When using single precision, it should produce the result: + +``` +Top 5 results: + +Samoyed (id=258): probability = 0.8846225142478943 +Arctic fox (id=279): probability = 0.045805174857378006 +white wolf (id=270): probability = 0.0442761555314064 +Pomeranian (id=259): probability = 0.005621383432298899 +Great Pyrenees (id=257): probability = 0.004652013536542654 +``` To save the pretrained ResNet-18 model to TorchScript run the modified version of the `pt2ts.py` tool : @@ -67,6 +81,15 @@ executable with an argument of the saved model file: ./resnet_infer_fortran ../saved_resnet18_model_cpu.pt ``` +This should produce the same top result: + +``` + Top result + + Samoyed (id= 259 ), : probability = 0.884623706 +``` + + Alternatively we can use `make`, instead of cmake, with the included Makefile. However, to do this you will need to modify `Makefile` to link to and include your installation of FTorch as described in the main documentation. Also check that the compiler is the same as the one you built the Library with. @@ -82,3 +105,48 @@ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib64 To explore the functionalities of this model: - Try saving the model through tracing rather than scripting by modifying `pt2ts.py` +- Try changing the input image. For example, running the following code will download an alternative image of a dog from [pytorch/vision](https://github.com/pytorch/vision/tree/v0.15.2/gallery/assets), saving it in the `data` directory: + +```python +import urllib + +url, filename = ( + "https://github.com/pytorch/vision/raw/v0.15.1/gallery/assets/dog1.jpg", + "data/dog2.jpg", +) +urllib.request.urlretrieve(url, filename) +``` + +`image_filename` in resnet18.py and resnet_infer_python.py must then be modified to match the name of this new file. + +`expected_prob` in resnet_infer_fortran.f90 and resnet_infer_python.py are also specific to the pre-downloaded example image, so either their values should be updated, or the assertions should be removed. + + +Note that the ImageNet labels can be downloaded and accessed similarly, using: + +```python +import urllib + +url, filename = ( + "https://raw.githubusercontent.com/pytorch/hub/e55b003/imagenet_classes.txt", + "imagenet_classes.txt", +) +data = urllib.request.urlopen(url) +categories = [s.strip().decode("utf-8") for s in data] +``` + +## When to transpose arrays? + +In this example, it is expected that the shape and indices of `in_data` in resnet_infer_fortran.f90 match that of `input_batch` in resnet18.py, i.e. `in_data(i, j, k, l) == input_batch[i, j, k, l]`. + +Since C is row-major (rows are contiguous in memory), whereas Fortran is column-major (columns are contiguous), it is therefore necessary to perform a transpose when converting from the NumPy array to the Fortran array to ensure that their indices are consistent. + +In this example code, the NumPy array is transposed before being flattened and saved to binary, allowing Fortran to `reshape` the flatted array into the correct order. + +An alternative would be to save the NumPy array with its original shape, but perform a transpose during or after reading the data into Fortran, e.g. using: + +``` +in_data = reshape(flat_data, shape(in_data), order=(4,3,2,1)) +``` + +For more general use, it should be noted that the function used to create the input tensor from `input_batch`, `torch_tensor_from_blob`, performs a further transpose, which is required to allow the tensor to interact correctly with the model. diff --git a/examples/2_ResNet18/data/categories.txt b/examples/2_ResNet18/data/categories.txt new file mode 100644 index 00000000..f40829ed --- /dev/null +++ b/examples/2_ResNet18/data/categories.txt @@ -0,0 +1,1000 @@ +tench +goldfish +great white shark +tiger shark +hammerhead +electric ray +stingray +cock +hen +ostrich +brambling +goldfinch +house finch +junco +indigo bunting +robin +bulbul +jay +magpie +chickadee +water ouzel +kite +bald eagle +vulture +great grey owl +European fire salamander +common newt +eft +spotted salamander +axolotl +bullfrog +tree frog +tailed frog +loggerhead +leatherback turtle +mud turtle +terrapin +box turtle +banded gecko +common iguana +American chameleon +whiptail +agama +frilled lizard +alligator lizard +Gila monster +green lizard +African chameleon +Komodo dragon +African crocodile +American alligator +triceratops +thunder snake +ringneck snake +hognose snake +green snake +king snake +garter snake +water snake +vine snake +night snake +boa constrictor +rock python +Indian cobra +green mamba +sea snake +horned viper +diamondback +sidewinder +trilobite +harvestman +scorpion +black and gold garden spider +barn spider +garden spider +black widow +tarantula +wolf spider +tick +centipede +black grouse +ptarmigan +ruffed grouse +prairie chicken +peacock +quail +partridge +African grey +macaw +sulphur-crested cockatoo +lorikeet +coucal +bee eater +hornbill +hummingbird +jacamar +toucan +drake +red-breasted merganser +goose +black swan +tusker +echidna +platypus +wallaby +koala +wombat +jellyfish +sea anemone +brain coral +flatworm +nematode +conch +snail +slug +sea slug +chiton +chambered nautilus +Dungeness crab +rock crab +fiddler crab +king crab +American lobster +spiny lobster +crayfish +hermit crab +isopod +white stork +black stork +spoonbill +flamingo +little blue heron +American egret +bittern +crane +limpkin +European gallinule +American coot +bustard +ruddy turnstone +red-backed sandpiper +redshank +dowitcher +oystercatcher +pelican +king penguin +albatross +grey whale +killer whale +dugong +sea lion +Chihuahua +Japanese spaniel +Maltese dog +Pekinese +Shih-Tzu +Blenheim spaniel +papillon +toy terrier +Rhodesian ridgeback +Afghan hound +basset +beagle +bloodhound +bluetick +black-and-tan coonhound +Walker hound +English foxhound +redbone +borzoi +Irish wolfhound +Italian greyhound +whippet +Ibizan hound +Norwegian elkhound +otterhound +Saluki +Scottish deerhound +Weimaraner +Staffordshire bullterrier +American Staffordshire terrier +Bedlington terrier +Border terrier +Kerry blue terrier +Irish terrier +Norfolk terrier +Norwich terrier +Yorkshire terrier +wire-haired fox terrier +Lakeland terrier +Sealyham terrier +Airedale +cairn +Australian terrier +Dandie Dinmont +Boston bull +miniature schnauzer +giant schnauzer +standard schnauzer +Scotch terrier +Tibetan terrier +silky terrier +soft-coated wheaten terrier +West Highland white terrier +Lhasa +flat-coated retriever +curly-coated retriever +golden retriever +Labrador retriever +Chesapeake Bay retriever +German short-haired pointer +vizsla +English setter +Irish setter +Gordon setter +Brittany spaniel +clumber +English springer +Welsh springer spaniel +cocker spaniel +Sussex spaniel +Irish water spaniel +kuvasz +schipperke +groenendael +malinois +briard +kelpie +komondor +Old English sheepdog +Shetland sheepdog +collie +Border collie +Bouvier des Flandres +Rottweiler +German shepherd +Doberman +miniature pinscher +Greater Swiss Mountain dog +Bernese mountain dog +Appenzeller +EntleBucher +boxer +bull mastiff +Tibetan mastiff +French bulldog +Great Dane +Saint Bernard +Eskimo dog +malamute +Siberian husky +dalmatian +affenpinscher +basenji +pug +Leonberg +Newfoundland +Great Pyrenees +Samoyed +Pomeranian +chow +keeshond +Brabancon griffon +Pembroke +Cardigan +toy poodle +miniature poodle +standard poodle +Mexican hairless +timber wolf +white wolf +red wolf +coyote +dingo +dhole +African hunting dog +hyena +red fox +kit fox +Arctic fox +grey fox +tabby +tiger cat +Persian cat +Siamese cat +Egyptian cat +cougar +lynx +leopard +snow leopard +jaguar +lion +tiger +cheetah +brown bear +American black bear +ice bear +sloth bear +mongoose +meerkat +tiger beetle +ladybug +ground beetle +long-horned beetle +leaf beetle +dung beetle +rhinoceros beetle +weevil +fly +bee +ant +grasshopper +cricket +walking stick +cockroach +mantis +cicada +leafhopper +lacewing +dragonfly +damselfly +admiral +ringlet +monarch +cabbage butterfly +sulphur butterfly +lycaenid +starfish +sea urchin +sea cucumber +wood rabbit +hare +Angora +hamster +porcupine +fox squirrel +marmot +beaver +guinea pig +sorrel +zebra +hog +wild boar +warthog +hippopotamus +ox +water buffalo +bison +ram +bighorn +ibex +hartebeest +impala +gazelle +Arabian camel +llama +weasel +mink +polecat +black-footed ferret +otter +skunk +badger +armadillo +three-toed sloth +orangutan +gorilla +chimpanzee +gibbon +siamang +guenon +patas +baboon +macaque +langur +colobus +proboscis monkey +marmoset +capuchin +howler monkey +titi +spider monkey +squirrel monkey +Madagascar cat +indri +Indian elephant +African elephant +lesser panda +giant panda +barracouta +eel +coho +rock beauty +anemone fish +sturgeon +gar +lionfish +puffer +abacus +abaya +academic gown +accordion +acoustic guitar +aircraft carrier +airliner +airship +altar +ambulance +amphibian +analog clock +apiary +apron +ashcan +assault rifle +backpack +bakery +balance beam +balloon +ballpoint +Band Aid +banjo +bannister +barbell +barber chair +barbershop +barn +barometer +barrel +barrow +baseball +basketball +bassinet +bassoon +bathing cap +bath towel +bathtub +beach wagon +beacon +beaker +bearskin +beer bottle +beer glass +bell cote +bib +bicycle-built-for-two +bikini +binder +binoculars +birdhouse +boathouse +bobsled +bolo tie +bonnet +bookcase +bookshop +bottlecap +bow +bow tie +brass +brassiere +breakwater +breastplate +broom +bucket +buckle +bulletproof vest +bullet train +butcher shop +cab +caldron +candle +cannon +canoe +can opener +cardigan +car mirror +carousel +carpenter's kit +carton +car wheel +cash machine +cassette +cassette player +castle +catamaran +CD player +cello +cellular telephone +chain +chainlink fence +chain mail +chain saw +chest +chiffonier +chime +china cabinet +Christmas stocking +church +cinema +cleaver +cliff dwelling +cloak +clog +cocktail shaker +coffee mug +coffeepot +coil +combination lock +computer keyboard +confectionery +container ship +convertible +corkscrew +cornet +cowboy boot +cowboy hat +cradle +crane +crash helmet +crate +crib +Crock Pot +croquet ball +crutch +cuirass +dam +desk +desktop computer +dial telephone +diaper +digital clock +digital watch +dining table +dishrag +dishwasher +disk brake +dock +dogsled +dome +doormat +drilling platform +drum +drumstick +dumbbell +Dutch oven +electric fan +electric guitar +electric locomotive +entertainment center +envelope +espresso maker +face powder +feather boa +file +fireboat +fire engine +fire screen +flagpole +flute +folding chair +football helmet +forklift +fountain +fountain pen +four-poster +freight car +French horn +frying pan +fur coat +garbage truck +gasmask +gas pump +goblet +go-kart +golf ball +golfcart +gondola +gong +gown +grand piano +greenhouse +grille +grocery store +guillotine +hair slide +hair spray +half track +hammer +hamper +hand blower +hand-held computer +handkerchief +hard disc +harmonica +harp +harvester +hatchet +holster +home theater +honeycomb +hook +hoopskirt +horizontal bar +horse cart +hourglass +iPod +iron +jack-o'-lantern +jean +jeep +jersey +jigsaw puzzle +jinrikisha +joystick +kimono +knee pad +knot +lab coat +ladle +lampshade +laptop +lawn mower +lens cap +letter opener +library +lifeboat +lighter +limousine +liner +lipstick +Loafer +lotion +loudspeaker +loupe +lumbermill +magnetic compass +mailbag +mailbox +maillot +maillot +manhole cover +maraca +marimba +mask +matchstick +maypole +maze +measuring cup +medicine chest +megalith +microphone +microwave +military uniform +milk can +minibus +miniskirt +minivan +missile +mitten +mixing bowl +mobile home +Model T +modem +monastery +monitor +moped +mortar +mortarboard +mosque +mosquito net +motor scooter +mountain bike +mountain tent +mouse +mousetrap +moving van +muzzle +nail +neck brace +necklace +nipple +notebook +obelisk +oboe +ocarina +odometer +oil filter +organ +oscilloscope +overskirt +oxcart +oxygen mask +packet +paddle +paddlewheel +padlock +paintbrush +pajama +palace +panpipe +paper towel +parachute +parallel bars +park bench +parking meter +passenger car +patio +pay-phone +pedestal +pencil box +pencil sharpener +perfume +Petri dish +photocopier +pick +pickelhaube +picket fence +pickup +pier +piggy bank +pill bottle +pillow +ping-pong ball +pinwheel +pirate +pitcher +plane +planetarium +plastic bag +plate rack +plow +plunger +Polaroid camera +pole +police van +poncho +pool table +pop bottle +pot +potter's wheel +power drill +prayer rug +printer +prison +projectile +projector +puck +punching bag +purse +quill +quilt +racer +racket +radiator +radio +radio telescope +rain barrel +recreational vehicle +reel +reflex camera +refrigerator +remote control +restaurant +revolver +rifle +rocking chair +rotisserie +rubber eraser +rugby ball +rule +running shoe +safe +safety pin +saltshaker +sandal +sarong +sax +scabbard +scale +school bus +schooner +scoreboard +screen +screw +screwdriver +seat belt +sewing machine +shield +shoe shop +shoji +shopping basket +shopping cart +shovel +shower cap +shower curtain +ski +ski mask +sleeping bag +slide rule +sliding door +slot +snorkel +snowmobile +snowplow +soap dispenser +soccer ball +sock +solar dish +sombrero +soup bowl +space bar +space heater +space shuttle +spatula +speedboat +spider web +spindle +sports car +spotlight +stage +steam locomotive +steel arch bridge +steel drum +stethoscope +stole +stone wall +stopwatch +stove +strainer +streetcar +stretcher +studio couch +stupa +submarine +suit +sundial +sunglass +sunglasses +sunscreen +suspension bridge +swab +sweatshirt +swimming trunks +swing +switch +syringe +table lamp +tank +tape player +teapot +teddy +television +tennis ball +thatch +theater curtain +thimble +thresher +throne +tile roof +toaster +tobacco shop +toilet seat +torch +totem pole +tow truck +toyshop +tractor +trailer truck +tray +trench coat +tricycle +trimaran +tripod +triumphal arch +trolleybus +trombone +tub +turnstile +typewriter keyboard +umbrella +unicycle +upright +vacuum +vase +vault +velvet +vending machine +vestment +viaduct +violin +volleyball +waffle iron +wall clock +wallet +wardrobe +warplane +washbasin +washer +water bottle +water jug +water tower +whiskey jug +whistle +wig +window screen +window shade +Windsor tie +wine bottle +wing +wok +wooden spoon +wool +worm fence +wreck +yawl +yurt +web site +comic book +crossword puzzle +street sign +traffic light +book jacket +menu +plate +guacamole +consomme +hot pot +trifle +ice cream +ice lolly +French loaf +bagel +pretzel +cheeseburger +hotdog +mashed potato +head cabbage +broccoli +cauliflower +zucchini +spaghetti squash +acorn squash +butternut squash +cucumber +artichoke +bell pepper +cardoon +mushroom +Granny Smith +strawberry +orange +lemon +fig +pineapple +banana +jackfruit +custard apple +pomegranate +hay +carbonara +chocolate sauce +dough +meat loaf +pizza +potpie +burrito +red wine +espresso +cup +eggnog +alp +bubble +cliff +coral reef +geyser +lakeside +promontory +sandbar +seashore +valley +volcano +ballplayer +groom +scuba diver +rapeseed +daisy +yellow lady's slipper +corn +acorn +hip +buckeye +coral fungus +agaric +gyromitra +stinkhorn +earthstar +hen-of-the-woods +bolete +ear +toilet tissue diff --git a/examples/2_ResNet18/data/dog.jpg b/examples/2_ResNet18/data/dog.jpg new file mode 100644 index 00000000..12f0e0dd Binary files /dev/null and b/examples/2_ResNet18/data/dog.jpg differ diff --git a/examples/2_ResNet18/pt2ts.py b/examples/2_ResNet18/pt2ts.py index 15fc48d0..b4b383c7 100644 --- a/examples/2_ResNet18/pt2ts.py +++ b/examples/2_ResNet18/pt2ts.py @@ -76,12 +76,14 @@ def load_torchscript(filename: Optional[str] = "saved_model.pt") -> torch.nn.Mod # Load model and prepare for saving # ===================================================== + precision = torch.float32 + # FPTLIB-TODO # Load a pre-trained PyTorch model # Insert code here to load your model as `trained_model`. # This example assumes my_ml_model has a method `initialize` to load # architecture, weights, and place in inference mode - trained_model = resnet18.initialize() + trained_model = resnet18.initialize(precision) # Switch off specific layers/parts of the model that behave # differently during training and inference. diff --git a/examples/2_ResNet18/resnet18.py b/examples/2_ResNet18/resnet18.py index 6d726ff1..0f8b070a 100644 --- a/examples/2_ResNet18/resnet18.py +++ b/examples/2_ResNet18/resnet18.py @@ -1,20 +1,30 @@ """Load and run pretrained ResNet-18 from TorchVision.""" +import numpy as np +from PIL import Image import torch -import torch.nn.functional as F import torchvision # Initialize everything -def initialize(): +def initialize(precision: torch.dtype) -> torch.nn.Module: """ Download pre-trained ResNet-18 model and prepare for inference. + Parameters + ---------- + precision: torch.dtype + Sets the working precision of the model. + Returns ------- - model : torch.nn.Module + model: torch.nn.Module + Pretrained ResNet-18 model """ + # Set working precision + torch.set_default_dtype(precision) + # Load a pre-trained PyTorch model print("Loading pre-trained ResNet-18 model...", end="") model = torchvision.models.resnet18(weights="IMAGENET1K_V1") @@ -27,24 +37,98 @@ def initialize(): return model -def run_model(model): +def run_model(model: torch.nn.Module, precision: type) -> None: """ - Run the pre-trained ResNet-18 with dummy input of ones. + Run the pre-trained ResNet-18 with an example image of a dog. Parameters ---------- - model : torch.nn.Module + model: torch.nn.Module + Pretrained model to run. + precision: type + NumPy data type to save input tensor. """ + # Transform image into the form expected by the pre-trained model, using the mean + # and standard deviation from the ImageNet dataset + # See: https://pytorch.org/vision/0.8/models.html + image_filename = "data/dog.jpg" + input_image = Image.open(image_filename) + preprocess = torchvision.transforms.Compose( + [ + torchvision.transforms.Resize(256), + torchvision.transforms.CenterCrop(224), + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize( + mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] + ), + ] + ) + input_tensor = preprocess(input_image) + input_batch = input_tensor.unsqueeze(0) + + print("Saving input batch...", end="") + # Transpose input before saving so order consistent with Fortran + np_input = np.array( + input_batch.numpy().transpose().flatten(), dtype=precision + ) # type: np.typing.NDArray - print("Running ResNet-18 model for ones...", end="") - dummy_input = torch.ones(1, 3, 224, 224) - output = model(dummy_input) - top5 = F.softmax(output, dim=1).topk(5).indices + # Save data as binary + np_input.tofile("data/image_tensor.dat") + + # Load saved data to check it was saved correctly + np_data = np.fromfile( + "data/image_tensor.dat", dtype=precision + ) # type: np.typing.NDArray + + # Reshape to original tensor shape + tensor_shape = np.array(input_batch.numpy()).transpose().shape + np_data = np_data.reshape(tensor_shape) + np_data = np_data.transpose() + assert np.array_equal(np_data, input_batch.numpy()) is True print("done.") - print(f"Top 5 results:\n {top5}") + print("Running ResNet-18 model for input...", end="") + with torch.no_grad(): + output = model(input_batch) + print("done.") + + print_top_results(output) + + +def print_top_results(output: torch.Tensor) -> None: + """Prints top 5 results + + Parameters + ---------- + output: torch.Tensor + Output from ResNet-18. + """ + # Run a softmax to get probabilities + probabilities = torch.nn.functional.softmax(output[0], dim=0) + + # Read ImageNet labels from text file + cats_filename = "data/categories.txt" + categories = np.genfromtxt(cats_filename, dtype=str, delimiter="\n") + + # Show top categories per image + top5_prob, top5_catid = torch.topk(probabilities, 5) + print("\nTop 5 results:\n") + for i in range(top5_prob.size(0)): + cat_id = top5_catid[i] + print( + f"{categories[cat_id]} (id={cat_id}): probability = {top5_prob[i].item()}" + ) if __name__ == "__main__": - rn_model = initialize() - run_model(rn_model) + np_precision = np.float32 + + if np_precision == np.float32: + torch_precision = torch.float32 + elif np_precision == np.float64: + torch_precision = torch.float64 + else: + raise ValueError("`np_precision` must be of type `np.float32` or `np.float64`") + + rn_model = initialize(torch_precision) + run_model(rn_model, np_precision) diff --git a/examples/2_ResNet18/resnet_infer_fortran.f90 b/examples/2_ResNet18/resnet_infer_fortran.f90 index 18930902..588d0abc 100644 --- a/examples/2_ResNet18/resnet_infer_fortran.f90 +++ b/examples/2_ResNet18/resnet_infer_fortran.f90 @@ -1,62 +1,216 @@ program inference ! Imports primitives used to interface with C - use, intrinsic :: iso_c_binding, only: c_int64_t, c_float, c_char, c_null_char, c_ptr, c_loc + use, intrinsic :: iso_c_binding, only: c_sp=>c_float, c_dp=>c_double, c_int64_t, c_null_char, c_loc + use, intrinsic :: iso_fortran_env, only : sp => real32, dp => real64 ! Import our library for interfacing with PyTorch - use ftorch + use :: ftorch implicit none - integer :: num_args, ix - character(len=128), dimension(:), allocatable :: args - - ! Set up types of input and output data and the interface with C - type(torch_module) :: model - type(torch_tensor), dimension(1) :: in_tensor - type(torch_tensor) :: out_tensor - - real(c_float), dimension(:,:,:,:), allocatable, target :: in_data - integer(c_int), parameter :: n_inputs = 1 - real(c_float), dimension(:,:), allocatable, target :: out_data - - integer(c_int), parameter :: in_dims = 4 - integer(c_int64_t) :: in_shape(in_dims) = [1, 3, 224, 224] - integer(c_int) :: in_layout(in_dims) = [1,2,3,4] - integer(c_int), parameter :: out_dims = 2 - integer(c_int64_t) :: out_shape(out_dims) = [1, 1000] - integer(c_int) :: out_layout(out_dims) = [1,2] - - ! Get TorchScript model file as a command line argument - num_args = command_argument_count() - allocate(args(num_args)) - do ix = 1, num_args - call get_command_argument(ix,args(ix)) - end do - - - ! Allocate one-dimensional input/output arrays, based on multiplication of all input/output dimension sizes - allocate(in_data(in_shape(1), in_shape(2), in_shape(3), in_shape(4))) - allocate(out_data(out_shape(1), out_shape(2))) - - ! Initialise data - in_data = 1.0d0 - - ! Create input/output tensors from the above arrays - in_tensor(1) = torch_tensor_from_blob(c_loc(in_data), in_dims, in_shape, torch_kFloat32, torch_kCPU, in_layout) - out_tensor = torch_tensor_from_blob(c_loc(out_data), out_dims, out_shape, torch_kFloat32, torch_kCPU, out_layout) - - ! Load ML model (edit this line to use different models) - model = torch_module_load(trim(args(1))//c_null_char) - - ! Infer - call torch_module_forward(model, in_tensor, n_inputs, out_tensor) - write (*,*) out_data(1, 1000) - - ! Cleanup - call torch_module_delete(model) - call torch_tensor_delete(in_tensor(1)) - call torch_tensor_delete(out_tensor) - deallocate(in_data) - deallocate(out_data) + ! Define working precision for C primitives + ! Precision must match `wp` in resnet18.py and `wp_torch` in pt2ts.py + integer, parameter :: c_wp = c_sp + integer, parameter :: wp = sp + integer, parameter :: torch_wp = torch_kFloat32 + + call main() + +contains + + subroutine main() + + implicit none + + integer :: num_args, ix + character(len=128), dimension(:), allocatable :: args + + ! Set up types of input and output data and the interface with C + type(torch_module) :: model + type(torch_tensor), dimension(1) :: in_tensor + type(torch_tensor) :: out_tensor + + real(c_wp), dimension(:,:,:,:), allocatable, target :: in_data + integer(c_int), parameter :: n_inputs = 1 + real(c_wp), dimension(:,:), allocatable, target :: out_data + + integer(c_int), parameter :: in_dims = 4 + integer(c_int64_t) :: in_shape(in_dims) = [1, 3, 224, 224] + integer(c_int) :: in_layout(in_dims) = [1,2,3,4] + integer(c_int), parameter :: out_dims = 2 + integer(c_int64_t) :: out_shape(out_dims) = [1, 1000] + integer(c_int) :: out_layout(out_dims) = [1,2] + + ! Binary file containing input tensor + character(len=*), parameter :: filename = '../data/image_tensor.dat' + ! Text file containing categories + character(len=*), parameter :: filename_cats = '../data/categories.txt' + + ! Length of tensor and number of categories + integer, parameter :: N = 150528 + integer, parameter :: N_cats = 1000 + + ! Outputs + integer :: index(2) + real(wp), dimension(:,:), allocatable :: probabilities + real(wp), parameter :: expected_prob = 0.8846225142478943 + character(len=100) :: categories(N_cats) + real(wp) :: probability + + ! Get TorchScript model file as a command line argument + num_args = command_argument_count() + allocate(args(num_args)) + do ix = 1, num_args + call get_command_argument(ix,args(ix)) + end do + + ! Allocate one-dimensional input/output arrays, based on multiplication of all input/output dimension sizes + allocate(in_data(in_shape(1), in_shape(2), in_shape(3), in_shape(4))) + allocate(out_data(out_shape(1), out_shape(2))) + allocate(probabilities(out_shape(1), out_shape(2))) + + call load_data(filename, N, in_data, in_dims, in_shape) + + ! Create input/output tensors from the above arrays + in_tensor(1) = torch_tensor_from_blob(c_loc(in_data), in_dims, in_shape, torch_wp, torch_kCPU, in_layout) + out_tensor = torch_tensor_from_blob(c_loc(out_data), out_dims, out_shape, torch_wp, torch_kCPU, out_layout) + + ! Load ML model (edit this line to use different models) + model = torch_module_load(trim(args(1))//c_null_char) + + ! Infer + call torch_module_forward(model, in_tensor, n_inputs, out_tensor) + + ! Load categories + call load_categories(filename_cats, N_cats, categories) + + ! Calculate probabilities and output results + call calc_probs(out_data, probabilities, out_dims, out_shape) + index = maxloc(probabilities) + probability = maxval(probabilities) + + ! Check top probability matches expected value + call assert_real(probability, expected_prob, test_name="Check probability", rtol_opt=1e-5) + + write (*,*) "Top result" + write (*,*) "" + write (*,*) trim(categories(index(2))), " (id=", index(2), "), : probability =", probability + + ! Cleanup + call torch_module_delete(model) + call torch_tensor_delete(in_tensor(1)) + call torch_tensor_delete(out_tensor) + deallocate(in_data) + deallocate(out_data) + deallocate(probabilities) + deallocate(args) + + end subroutine main + + subroutine load_data(filename, N, in_data, in_dims, in_shape) + + implicit none + + character(len=*), intent(in) :: filename + integer, intent(in) :: N + real(c_wp), dimension(:,:,:,:), intent(out) :: in_data + + integer(c_int), intent(in) :: in_dims + integer(c_int64_t), intent(in) :: in_shape(in_dims) + + real(c_wp) :: flat_data(N) + integer :: ios, count, idx_1, idx_2, idx_3, idx_4 + character(len=100) :: ioerrmsg + + ! Read input tensor from Python script + open(unit=10, file=filename, status='old', access='stream', form='unformatted', action="read", iostat=ios, iomsg=ioerrmsg) + if (ios /= 0) then + print *, ioerrmsg + stop 1 + end if + + read(10, iostat=ios, iomsg=ioerrmsg) flat_data + if (ios /= 0) then + print *, ioerrmsg + stop 1 + end if + + close(10) + + ! Reshape data to tensor input shape + ! This assumes the data from Python was transposed before saving + in_data = reshape(flat_data, shape(in_data)) + + end subroutine load_data + + subroutine load_categories(filename_cats, N_cats, categories) + + implicit none + + character(len=*), intent(in) :: filename_cats + integer, intent(in) :: N_cats + character(len=100), intent(out) :: categories(N_cats) + + integer :: ios, i + character(len=100) :: ioerrmsg + + open (unit=11, file=filename_cats, form='formatted', access='stream', action='read', iostat=ios, iomsg=ioerrmsg) + if (ios /= 0) then + print *, ioerrmsg + stop 1 + end if + + read(11, '(a)') categories + close(11) + + end subroutine load_categories + + subroutine calc_probs(out_data, probabilities, out_dims, out_shape) + + implicit none + + integer(c_int), intent(in) :: out_dims + integer(c_int64_t), intent(in) :: out_shape(out_dims) + real(c_wp), dimension(:,:), intent(in) :: out_data + real(wp), dimension(:,:), intent(out) :: probabilities + real(wp) :: prob_sum + integer :: i, j + + ! Apply softmax function to calculate probabilties + probabilities = exp(out_data) + prob_sum = sum(probabilities) + probabilities = probabilities / prob_sum + + end subroutine calc_probs + + subroutine assert_real(a, b, test_name, rtol_opt) + + implicit none + + character(len=*) :: test_name + real, intent(in) :: a, b + real, optional :: rtol_opt + real :: relative_error, rtol + + character(len=15) :: pass, fail + + fail = char(27)//'[31m'//'FAILED'//char(27)//'[0m' + pass = char(27)//'[32m'//'PASSED'//char(27)//'[0m' + + if (.not. present(rtol_opt)) then + rtol = 1e-5 + else + rtol = rtol_opt + end if + + relative_error = abs(a/b - 1.) + + if (relative_error > rtol) then + write(*, '(A, " :: [", A, "] maximum relative error = ", E11.4)') fail, trim(test_name), relative_error + else + write(*, '(A, " :: [", A, "] maximum relative error = ", E11.4)') pass, trim(test_name), relative_error + end if + + end subroutine assert_real end program inference diff --git a/examples/2_ResNet18/resnet_infer_python.py b/examples/2_ResNet18/resnet_infer_python.py index a1dd4993..fbae561c 100644 --- a/examples/2_ResNet18/resnet_infer_python.py +++ b/examples/2_ResNet18/resnet_infer_python.py @@ -1,11 +1,14 @@ -"""Load ResNet-18 saved to TorchScript and run inference with ones.""" +"""Load ResNet-18 saved to TorchScript and run inference with an example image.""" +from math import isclose +import numpy as np import torch +from resnet18 import print_top_results -def deploy(saved_model, device, batch_size=1): +def deploy(saved_model: str, device: str, batch_size: int = 1) -> torch.Tensor: """ - Load TorchScript ResNet-18 and run inference with Tensor of ones. + Load TorchScript ResNet-18 and run inference with Tensor from example image. Parameters ---------- @@ -21,8 +24,13 @@ def deploy(saved_model, device, batch_size=1): output : torch.Tensor result of running inference on model with Tensor of ones """ + transposed_shape = [224, 224, 3, batch_size] + precision = np.float32 - input_tensor = torch.ones(batch_size, 3, 224, 224) + np_data = np.fromfile("data/image_tensor.dat", dtype=precision) + np_data = np_data.reshape(transposed_shape) + np_data = np_data.transpose() + input_tensor = torch.from_numpy(np_data) if device == "cpu": # Load saved TorchScript model @@ -42,14 +50,32 @@ def deploy(saved_model, device, batch_size=1): return output +def check_results(output: torch.Tensor) -> None: + """ + Compare top model output to expected result. + + Parameters + ---------- + output: torch.Tensor + Output from ResNet-18. + """ + expected_prob = 0.8846225142478943 + # Run a softmax to get probabilities + assert isclose( + torch.max(torch.nn.functional.softmax(output[0], dim=0)), + expected_prob, + abs_tol=1e-8, + ) + + if __name__ == "__main__": saved_model_file = "saved_resnet18_model_cpu.pt" device_to_run = "cpu" - # device = "cuda" + # device_to_run = "cuda" batch_size_to_run = 1 result = deploy(saved_model_file, device_to_run, batch_size_to_run) - - print(result[:, 0:5]) + print_top_results(result) + check_results(result) diff --git a/examples/n_c_and_cpp/resnet_infer_python.py b/examples/n_c_and_cpp/resnet_infer_python.py index 8efdc64e..d5b462a3 100644 --- a/examples/n_c_and_cpp/resnet_infer_python.py +++ b/examples/n_c_and_cpp/resnet_infer_python.py @@ -1,6 +1,7 @@ """Load ResNet-18 saved to TorchScript and run inference with ones.""" import torch +from PIL import Image def deploy(saved_model, device, batch_size=1): @@ -22,7 +23,20 @@ def deploy(saved_model, device, batch_size=1): result of running inference on model with Tensor of ones """ - input_tensor = torch.ones(batch_size, 3, 224, 224) + image_filename = "data/dog.jpg" + input_image = Image.open(image_filename) + preprocess = torchvision.transforms.Compose( + [ + torchvision.transforms.Resize(256), + torchvision.transforms.CenterCrop(224), + torchvision.transforms.ToTensor(), + torchvision.transforms.Normalize( + mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225] + ), + ] + ) + input_tensor = preprocess(input_image) + input_batch = input_tensor.unsqueeze(0) if device == "cpu": # Load saved TorchScript model @@ -43,7 +57,6 @@ def deploy(saved_model, device, batch_size=1): if __name__ == "__main__": - saved_model_file = "saved_resnet18_model_cpu.pt" device_to_run = "cpu"