Here is the raw output of my mode on iOS xcode environment:
where the classification is totally wrong, the 23th tensor 7.87…
override func viewDidLoad() {
        super.viewDidLoad()
        let image = UIImage(named: "frame28.jpg")!
        imageView.image = image
        let resizedImage = image.resized(to: CGSize(width: 224, height: 224))
        guard var pixelBuffer = resizedImage.normalized() else {
            return
        }
        guard let outputs = module.predict(image: UnsafeMutableRawPointer(&pixelBuffer)) else {
            return
        }
        print(outputs)
        let zippedResults = zip(labels.indices, outputs)
        print(labels.indices)
        let sortedResults = zippedResults.sorted { $0.1.floatValue > $1.1.floatValue }.prefix(3)
        
        var text = ""
        for result in sortedResults {
            text += "\u{2022} \(labels[result.0]) \n\n"
        }
        resultView.text = text

result:
[0.7592794, 1.170417, 0.4990637, 4.677132, -0.9478517, -2.447758, 1.918674, 0.5767481, 1.738559, -0.2370548, 0.4759183, 2.106035, -0.5786518, -1.511588, -2.636765, -2.717191, -0.9784743, -0.7371585, -2.024575, -2.237826, -2.281566, -0.03008107, 7.870783, 1.824642, 0.3037256, -0.4274126, 0.4281527, -0.4137115, -2.859639, -2.277968, -1.029291, -0.2246231, 1.358673, 2.990178, 2.051248, -0.05761524, 2.349458, 0.004223851, -1.494618, -2.429391]
Here is the code of Python validation:
# sample execution (requires torchvision)

import torch
from PIL import Image
from torchvision import datasets, models, transforms
import os

data_transforms = {
    'train': transforms.Compose([
        transforms.RandomResizedCrop(224),
        transforms.ColorJitter(brightness=1, contrast=1, saturation=1, hue=0.5),
        transforms.RandomRotation(degrees=15),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
    'val': transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

input_image = Image.open('./20200328_data/EA2020Test/val/Class3/frame28.jpg')
preprocess = transforms.Compose([
    transforms.Resize(256),
    transforms.CenterCrop(224),
    transforms.ToTensor(),
    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) # create a mini-batch as expected by the model

data_dir = '20200328_data/EA2020Test'
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
                                          data_transforms[x])
                  for x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
                                             shuffle=True, num_workers=4)
              for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
class_names = image_datasets['train'].classes



model_conv = torch.jit.load('mymodel-jit-cpu.pt')
model_conv.eval()

# move the input and model to GPU for speed if available
if torch.cuda.is_available():
    input_batch = input_batch.to('cuda')
    model.to('cuda')

with torch.no_grad():
    output = model_conv(input_batch)
    _, preds = torch.max(output, 1)
print(class_names[preds[0]])
print(output)
# The output has unnormalized scores. To get probabilities, you can run a softmax on it.
# print(torch.nn.functional.softmax(output[0], dim=0))

And here is the output tensor:
tensor([[ 3.0214e+00,  3.8240e-01, -9.9551e-01,  3.9651e+00,  2.6704e+00,
-1.5943e+00,  9.1830e-01, -6.0747e-03, -1.3737e+00, -6.4241e-01,
5.8611e-01,  2.2428e+00, -1.2174e+00, -1.3305e-01, -2.4817e+00,
-1.5725e+00, -1.0427e+00, -2.0795e+00, -2.4415e+00, -2.3984e-01,
-2.3185e+00, -9.6899e-02,  9.8113e+00,  1.5801e-01, -8.7105e-01,
-1.9445e+00, -5.7967e-01, -2.9598e-01, -1.9398e+00, -1.6080e+00,
-7.6736e-01, -9.2793e-01,  1.9376e+00,  1.9328e+00,  1.3377e+00,
-1.8019e-01,  2.2197e+00, -7.0646e-02, -2.4584e-01, -1.6802e+00]])
And Python classificaiton is correct, where the 3rd class (Class3) is labeled.
I got some feeling it might be some difference between torch.max (Python validation) and torch.exp
(Torch Mobile)?
here is the code of TorchModule.mm
#import "TorchModule.h"
#import <LibTorch/LibTorch.h>

@implementation TorchModule {
 @protected
  torch::jit::script::Module _impl;
}

- (nullable instancetype)initWithFileAtPath:(NSString*)filePath {
  self = [super init];
  if (self) {
    try {
      auto qengines = at::globalContext().supportedQEngines();
      if (std::find(qengines.begin(), qengines.end(), at::QEngine::QNNPACK) != qengines.end()) {
        at::globalContext().setQEngine(at::QEngine::QNNPACK);
      }
      _impl = torch::jit::load(filePath.UTF8String);
      _impl.eval();
    } catch (const std::exception& exception) {
      NSLog(@"%s", exception.what());
      return nil;
    }
  }
  return self;
}

- (NSArray<NSNumber*>*)predictImage:(void*)imageBuffer {
  try {
    at::Tensor tensor = torch::from_blob(imageBuffer, {1, 3, 224, 224}, at::kFloat);
    torch::autograd::AutoGradMode guard(false);
    at::AutoNonVariableTypeMode non_var_type_mode(true);
    auto outputTensor = _impl.forward({tensor}).toTensor();
    float* floatBuffer = outputTensor.data_ptr<float>();
    if (!floatBuffer) {
      return nil;
    }
    NSMutableArray* results = [[NSMutableArray alloc] init];
    for (int i = 0; i < 40; i++) {
      [results addObject:@(floatBuffer[i])];
    }
    return [results copy];
  } catch (const std::exception& exception) {
    NSLog(@"%s", exception.what());
  }
  return nil;
}

@end


thansk a lot for your help!