Embarcadero / python4delphi

Fork of P4D adding FireMonkey wrapper and Android support
MIT License
85 stars 19 forks source link

Python and Skia don't mix #32

Open peardox opened 2 years ago

peardox commented 2 years ago

I'm not positive about the exact criteria yet

Basically imports with Skia fully enabled causes Skia to hit problem with floating point

Take anything with an import and add this to your dproj (after installing Skia of course) and everything will go badly wrong.

Masking the FPU exception doesn't seem to help

As I don't know which package is causing the issue this is posted here

uses
  Skia, Skia.FMX, , ....

begin
  GlobalUseSkia := True;
  GlobalUseSkiaRasterWhenAvailable := False;
.
.
viniciusfbb commented 2 years ago

Hello, The way Python4delphi is changing exception masks is not secure. Care is needed mainly to remove exceptions from the mask. It's something that affects the entire application and other libraries like Skia, OpenGL, ActiveX, etc...

The problem is specifically in the MaskFPUExceptions function in PythonEngine.pas, which is used in the initialization of P4D-Data- Sciences

There are a few ways to resolve this. One of them would be to create a class to store the mask's state, change it, so that after a try finally you can restore the previous mask.

peardox commented 2 years ago

@viniciusfbb Thanks for the rapid reply and explanation. I sorta thought this was most likely MaskFPUExceptions (hence the issue being tentatively in this repo)

I wrote a replacement for MaskFPUExceptions that stores the current GetExceptionMask when called with True in a global then replaces it when called with false. This worked a treat and Skia now works perfectly again (Skia was really acting up without this fix...).

@lmbelo While my hack works I'm not submitting a PR for this one as this is a complicated issue which is situational - my 'fix' works for my app but other packages may well mess with the Exception Mask so a more robust solution is really required.

For reference here's what I did - you can enable/disable it via the define (good for testing)...

{$DEFINE USESAFEMASK}

var
  FPUMASK: TArithmeticExceptionMask;

procedure SafeMaskFPUExceptions(ExceptionsMasked : boolean;
  MatchPythonPrecision : Boolean = True);

implementation

uses
  Maths;

{$R *.fmx}

procedure SafeMaskFPUExceptions(ExceptionsMasked : boolean;
  MatchPythonPrecision : Boolean);
begin
  {$IFDEF USESAFEMASK}
  {$IF Defined(CPUX86) or Defined(CPUX64)}
  if ExceptionsMasked then
    begin
    FPUMASK := GetExceptionMask;
    SetExceptionMask([exInvalidOp, exDenormalized, exZeroDivide,
      exOverflow, exUnderflow, exPrecision]);
    end
  else
    SetExceptionMask(FPUMASK);
  {$WARN SYMBOL_PLATFORM OFF}
  {$IF Defined(FPC) or Defined(MSWINDOWS)}
  if MatchPythonPrecision then
      SetPrecisionMode(pmDouble)
    else
      SetPrecisionMode(pmExtended);
  {$WARN SYMBOL_PLATFORM ON}
  {$IFEND}
  {$IFEND}
  {$ELSE}
    MaskFPUExceptions(ExceptionsMasked, MatchPythonPrecision);
  {$IFEND}
end;
lmbelo commented 2 years ago

The exception mask is undone after the importation of any of the Python packages. If you have a look at the exception mask routine you'll see that the exception mask is being undone (for CPUX86 and CPUX64) to Delphi's default. If we're changing exception mask in somewhere out of modules importation, we should be restoring to the defaults when we're done. Can you show specifically where it is breaking down? Which platform are you targeting to? Hope I can be of any help.

peardox commented 2 years ago

Ahh, that explains why using afterimport to reset the mask didn't work...

I tried two methods...

import(packagea) import(packageb) import(packagec)

with each of those using shared beforeinstall, afterinstall, beforeimport, afterimport which calls my SafeMaskFPUExceptions() function - this didn't work :(

So, I've not got

SafeMaskFPUExceptions(True) import(packagea) SafeMaskFPUExceptions(False)

See https://github.com/peardox/Lartis/blob/main/PythonSystem.pas#L501

lmbelo commented 2 years ago

You should decorate it by a try finally block.

MaskFPUExceptions(true);
try
  //do whatever you want to do
finally
  MaskFPUExceptions(false);
end;
peardox commented 2 years ago

I'm going to - this was a recent test, still unfinished - no proper error handlers yet

I just discovered that I need to handle errors properly anyway (Linux screws up horribly ATM - Issue coming soon...)

lmbelo commented 2 years ago

You need to get extra attention when targeting installed Python versions on Linux. You might hit across known issues reported before here. Have a look in all closed issues when you're going afters Linux errors. Only for the sake of getting you ahead, you can't use apt installed Python working with the DelphiFMX library. We need a Python executable linked statically or dynamically with the interpreter shared library, or an executable that exposes interpreter's symbols some how. The is not the case of Python available by apt.

peardox commented 2 years ago

I was using an Embedded Python 3.9 - which is why the issue is listed here

lmbelo commented 2 years ago

Our embeddables should work fine on any of the provided platforms.

viniciusfbb commented 2 years ago

@lmbelo, you didn't understand my post. The P4D-Data-Sciences code is wrong and python4delphi's MaskFPUExceptions implementation should be changed as well. I will explain it better.

MaskFPUExceptions code from python4delphi:

https://github.com/Embarcadero/python4delphi/blob/19192adf85a68e513ab097c6a7bde219dd6348c1/Source/PythonEngine.pas#L9288-L9306

Note that the code above arbitrarily changes the exception mask to:

[exInvalidOp, exDenormalized, exZeroDivide, exOverflow, exUnderflow, exPrecision] when the ExceptionsMasked parameter is True [exDenormalized, exUnderflow, exPrecision] when the ExceptionsMasked parameter is False

Now, look at one of the P4D-Data-Sciences import code:

procedure TTorchVision.ImportModule;
begin
  MaskFPUExceptions(true);
  try
    inherited;
  finally
    MaskFPUExceptions(false);
  end;
end;

As it stands, if the exception mask before the code above is empty [], after import it will be [exDenormalized, exUnderflow, exPrecision]. Likewise, if the exception mask is exAllArithmeticExceptions (all exceptions), which is what happens when using OpenGL, D2D, GDIP, Skia, among others, after import the mask will be [exDenormalized, exUnderflow, exPrecision]. And there is no way to predict the mask that will be configured in the application when it arrives at the P4D-Data-Sciences import code, there are many variants, there are several other libraries that change like the Delphi XML lib itself, and it can even vary according to the application (VCL or FMX).

So, the correct thing is: save the mask value, change it and after importing the P4D-Data-Sciences, restore the previous mask value and not arbitrarily change it to [exDenormalized, exUnderflow, exPrecision].

I made a possible fix for both codes. First for python4delphi's MaskFPUExceptions:

procedure MaskFPUExceptions(ExceptionsMasked: Boolean;
  var StoredMask: TArithmeticExceptionMask; MatchPythonPrecision: Boolean);
begin
  {$IF Defined(CPUX86) or Defined(CPUX64)}
  if ExceptionsMasked then
    StoredMask := SetExceptionMask(GetExceptionMask + [exInvalidOp, exDenormalized, exZeroDivide,
      exOverflow, exUnderflow, exPrecision])
  else
    SetExceptionMask(StoredMask + [exDenormalized, exUnderflow, exPrecision]); // Maybe a SetExceptionMask(StoredMask); would be better, but I'm not sure if removing the " + [exDenormalized, exUnderflow, exPrecision]" would cause problems in python4delphi
  {$WARN SYMBOL_PLATFORM OFF}
  {$IF Defined(FPC) or Defined(MSWINDOWS)}
  if MatchPythonPrecision then
      SetPrecisionMode(pmDouble)
    else
      SetPrecisionMode(pmExtended);
  {$WARN SYMBOL_PLATFORM ON}
  {$IFEND}
  {$IFEND}
end;

And now one of the P4D-Data-Sciences import codes:

procedure TTorchVision.ImportModule;
var
  StoredMask: TArithmeticExceptionMask;
begin
  MaskFPUExceptions(true, StoredMask);
  try
    inherited;
  finally
    MaskFPUExceptions(false, StoredMask);
  end;
end;
lmbelo commented 2 years ago

@lmbelo, you didn't understand my post. The P4D-Data-Sciences code is wrong and python4delphi's MaskFPUExceptions implementation should be changed as well. I will explain it better.

MaskFPUExceptions code from python4delphi:

https://github.com/Embarcadero/python4delphi/blob/19192adf85a68e513ab097c6a7bde219dd6348c1/Source/PythonEngine.pas#L9288-L9306

Note that the code above arbitrarily changes the exception mask to:

[exInvalidOp, exDenormalized, exZeroDivide, exOverflow, exUnderflow, exPrecision] when the ExceptionsMasked parameter is True [exDenormalized, exUnderflow, exPrecision] when the ExceptionsMasked parameter is False

Now, look at one of the P4D-Data-Sciences import code:

procedure TTorchVision.ImportModule;
begin
  MaskFPUExceptions(true);
  try
    inherited;
  finally
    MaskFPUExceptions(false);
  end;
end;

As it stands, if the exception mask before the code above is empty [], after import it will be [exDenormalized, exUnderflow, exPrecision]. Likewise, if the exception mask is exAllArithmeticExceptions (all exceptions), which is what happens when using OpenGL, D2D, GDIP, Skia, among others, after import the mask will be [exDenormalized, exUnderflow, exPrecision]. And there is no way to predict the mask that will be configured in the application when it arrives at the P4D-Data-Sciences import code, there are many variants, there are several other libraries that change like the Delphi XML lib itself, and it can even vary according to the application (VCL or FMX).

So, the correct thing is: save the mask value, change it and after importing the P4D-Data-Sciences, restore the previous mask value and not arbitrarily change it to [exDenormalized, exUnderflow, exPrecision].

I made a possible fix for both codes. First for python4delphi's MaskFPUExceptions:

procedure MaskFPUExceptions(ExceptionsMasked: Boolean;
  var StoredMask: TArithmeticExceptionMask; MatchPythonPrecision: Boolean);
begin
  {$IF Defined(CPUX86) or Defined(CPUX64)}
  if ExceptionsMasked then
    StoredMask := SetExceptionMask(GetExceptionMask + [exInvalidOp, exDenormalized, exZeroDivide,
      exOverflow, exUnderflow, exPrecision])
  else
    SetExceptionMask(StoredMask + [exDenormalized, exUnderflow, exPrecision]); // Maybe a SetExceptionMask(StoredMask); would be better, but I'm not sure if removing the " + [exDenormalized, exUnderflow, exPrecision]" would cause problems in python4delphi
  {$WARN SYMBOL_PLATFORM OFF}
  {$IF Defined(FPC) or Defined(MSWINDOWS)}
  if MatchPythonPrecision then
      SetPrecisionMode(pmDouble)
    else
      SetPrecisionMode(pmExtended);
  {$WARN SYMBOL_PLATFORM ON}
  {$IFEND}
  {$IFEND}
end;

And now one of the P4D-Data-Sciences import codes:

procedure TTorchVision.ImportModule;
var
  StoredMask: TArithmeticExceptionMask;
begin
  MaskFPUExceptions(true, StoredMask);
  try
    inherited;
  finally
    MaskFPUExceptions(false, StoredMask);
  end;
end;

@viniciusfbb does Skia changes the exception mask for the whole app life cycle?

peardox commented 2 years ago

Just to complicate things ARM can emulate the FPU mask and Mac can be x64 or aarch64

viniciusfbb commented 2 years ago

@viniciusfbb does Skia changes the exception mask for the whole app life cycle?

Yes! OpenGL too.

lmbelo commented 2 years ago

As required by TWebBrowser, but it only changes as needed, right? Many external libraries changes it and restores as done.

lmbelo commented 2 years ago

We could stand with the following:

var LStoredState := System.Math.GetExceptionMask();
try
  MaskFPUExceptions(true);
  try
    //do whatever you want to do
  finally
    MaskFPUExceptions(false);
  end;
finally
  System.Math.SetExceptionMask(LStoredState);
end;

Otherwise, I'd need to change it in P4D.

lmbelo commented 2 years ago

As required by TWebBrowser, but it is only changes as needed, right? Many external libraries changes it and restored as done.

We are only changing the exception mask per invocation, otherwise we would get it incompatible with many other libraries.

viniciusfbb commented 2 years ago

We are only changing the exception mask per invocation, otherwise we would get it incompatible with many other libraries.

Changing the exception mask in general is already a risk, even if temporarily, as it affects the entire application, even threads running in parallel. But the biggest risk is in removing exceptions from the mask. That's why I suggested changing the code of python4delphi.PythonEngine.MaskFPUExceptions() to give include in the exception mask instead of completely replacing it to a new value.

lmbelo commented 2 years ago

Well, regarding Windows x86 (made by FLDCW and FSTCW ???) and x64, the FPSCR flag equivalent is saved across thread context switches. I'm almost sure ARM has a similar approach, I think they save it in the _FPSCR array (I need to confirm this). Those masks we are setting up on P4D are the ones required by Python.

viniciusfbb commented 2 years ago

Well, regarding Windows x86 and x64, the FPSCR flag is saved across thread context switches. I'm almost sure ARM has a similar approach, I think they seve it in the _FPSCR array (I need to confirm this). Those masks we are setting up on P4D are the ones required by Python.

Not quite. Change the main, run a thread and you will see the value of the mask of the main in the thread... Until a few years ago SetExceptionMask was not even thread safe, but I believe this has already been fixed.

Anyway, just changing the import of P4D-Data-Sciences the way you suggest is not the way I think is the most correct but it will hardly cause problems.

lmbelo commented 2 years ago

Well, just tried it for the sake of testing. Definitely changing it on a second thread doesn't affect the main thread. It proves that it is saved across thread context switches.

Screen Shot 2022-08-21 at 15 20 59

Screen Shot 2022-08-21 at 15 21 21

Still not sure that doing that change is the way to go.

lmbelo commented 2 years ago

As did by FMX.Canvas.GDIP, changes made on masks are done per invocation and undone after it. Changing exception masks for app life cycle can be risk.

Screen Shot 2022-08-21 at 15 30 25

viniciusfbb commented 2 years ago

I didn't say that changes to background threads would affect the main thread's exception mask. In my view, threads being created in the background would have the initial values of exception mask of the main thread.

However, I have confirmed it through testing, and it is not true. New threads copy the values of mask exceptions from the thread that created it, not from the main thread. So your change is safe too.

lmbelo commented 2 years ago

@viniciusfbb let me see where Skia is changing exception masks. By now, let's stand to the adjustments in the DSC components only.

peardox commented 2 years ago

An important concern is what happens when you call python outside P4D e.g. I'm using my hack above like this

    SafeMaskFPUExceptions(True);
    PyEng.ExecStrings(Shim);
    SafeMaskFPUExceptions(False);

I'm guessing I should also wrap e.g. MainModule.delphi_style();

As well.

MainModule.delphi_train(); can take a few hours to run (and will be using Skia once a second for reporting) - I'll have to give that a go this week

lmbelo commented 2 years ago

That's why exception masks should be changed and restored per invocation.

lmbelo commented 2 years ago

An important concern is what happens when you call python outside P4D e.g. I'm using my hack above like this

    SafeMaskFPUExceptions(True);
    PyEng.ExecStrings(Shim);
    SafeMaskFPUExceptions(False);

I'm guessing I should also wrap e.g. MainModule.delphi_style();

As well.

MainModule.delphi_train(); can take a few hours to run (and will be using Skia once a second for reporting) - I'll have to give that a go this week

Btw, how are you running a blocking task on main thread and using Skia to report it out of a second thread?

peardox commented 2 years ago

What's exacrly what SafeMaskFPUExceptions() does.

viniciusfbb commented 2 years ago

@viniciusfbb let me see where Skia is changing exception masks.

There are numerous operations that depend on masks in exceptions, which are executed every frame of the app's rendering, so we decided to change it already on load. https://github.com/skia4delphi/skia4delphi/blob/f61e4cc05842e5c8c6f7f6ce340ea2df149032c8/Source/Skia.API.pas#L2103

peardox commented 2 years ago

Just starting a new thread for the module call then hand back to Delphi

Yesterday they finally told us they have their own task manager (but it'll also be setting the mask to bad values I guess)

lmbelo commented 2 years ago

@viniciusfbb let me see where Skia is changing exception masks.

There are numerous operations that depend on masks in exceptions, which are executed every frame of the app's rendering, so we decided to change it already on load. https://github.com/skia4delphi/skia4delphi/blob/f61e4cc05842e5c8c6f7f6ce340ea2df149032c8/Source/Skia.API.pas#L2103

I understand. My concern is that it would affect other libraries out of our sight.

lmbelo commented 2 years ago

Just starting a new thread for the module call then hand back to Delphi

Yesterday they finally told us they have their own task manager (but it'll also be setting the mask to bad values I guess)

You must be careful while using the Python packages out of the main thread.

peardox commented 2 years ago

This seems to work...

  SafeMaskFPUExceptions(True);
  FTask := TTask.Run(
    procedure()
      begin
        TThread.Synchronize(nil,
          procedure()
          begin
            MainModule.delphi_style();
          end
          )
      end
    );
  SafeMaskFPUExceptions(False);

It uses a Skia shader while the thread is running showing data reported back from that thread (it's a boring progress display)