Skip to content

Commit

Permalink
Consolidate virtual function pages (#529)
Browse files Browse the repository at this point in the history
* added info about corner case with call from implementationPtr that works depending on the compiler optimization rules

* removed old page in usecases

* moved used images into programming guide folder

* added new relative paths for images in programming guide page
  • Loading branch information
JBludau authored Jun 4, 2024
1 parent fd2668a commit 666841b
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 181 deletions.
67 changes: 64 additions & 3 deletions docs/source/ProgrammingGuide/Kokkos-and-Virtual-Functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ At a glance this should be fine, we've made a device instance of a class, copied
Virtual functions allow a program to handle Derived classes through a pointer to their Base class and have things work as they should. To make this work, the compiler needs some way to identify whether a pointer which is nominally to a Base class really is a pointer to the Base, or whether it's really a pointer to any Derived class. This happens through VPointers and VTables. For every class with virtual functions, there is one VTable shared among all instances, this table contains function pointers for all the virtual functions the class implements.
![VTable](../usecases/VirtualFunctions-VTables.png)
![VTable](./figures/VirtualFunctions-VTables.png)
Okay, so now we have VTables, if a class knows what type it is it could call the correct function. But how does it know?
Remember that we have one VTable shared amongst all instances of a type. Each instance, however, has a hidden member called the VPointer, which on initialization the compiler points at the correct table. So a call to a virtual function simply dereferences that pointer, and then indexes into the VTable to find the precise virtual function called.
![VPointer](../usecases/VirtualFunctions-VPointers.png)
![VPointer](./figures/VirtualFunctions-VPointers.png)
Now that we know what the compiler is doing to implement virtual functions, we'll look at why it doesn't work with GPU's
Expand Down Expand Up @@ -111,6 +111,67 @@ deviceInstance->setAField(someHostValue); // set some field on the host

This is the solution that the code teams we have talked to have said is the most productive way to solve the problem.

## But what if I do not really need the V-Tables on the device side?
Consider the following example which calls the `virtual operator()` on the device from a pointer of derived class type.
One might think this should work because no V-Table lookup on the device is neccessary.
```c++
#include <Kokkos_Core.hpp>
#include <cstdio>

struct Interface
{
KOKKOS_DEFAULTED_FUNCTION
virtual ~Interface() = default;

KOKKOS_FUNCTION
virtual void operator()( const size_t) const = 0;
};

struct Implementation : public Interface
{
KOKKOS_FUNCTION
void operator()(const size_t i) const override
{ printf("%zu from Implementation\n", i); }

void apply(){
Kokkos::parallel_for("myLoop",10,
KOKKOS_CLASS_LAMBDA (const size_t i) { this->operator()(i); }
);
}
};

int main (int argc, char *argv[])
{
Kokkos::initialize(argc,argv);
{
auto implementationPtr = std::make_shared<Implementation>();
implementationPtr->apply();
Kokkos::fence();
}
Kokkos::finalize();
}
```
### Why is this not portable?
Inside the `parallel_for` the `operator()` is called. As `Implementation` derives from the pure virtual class `Interface`, the 'operator()' is marked `override`.
On ROCm 5.2 this results in a memory access violation.
When executing the `this->operator()(i)` call, the runtime looks into the V-Table and dereferences a host function pointer on the device.
### But if that is the case, why does it work with NVCC?
Notice, that the `parallel_for` is called from a pointer of type `Implementation` and not a pointer of type `Interface` pointing to an `Implementation` object.
Thus, no V-Table lookup for the `operator()` would be necessary as it can be deduced from the context of the call that it will be `Implementation::operator()`.
But here it comes down to how the compiler handles the lookup. NVCC understands that the call is coming from an `Implementation` object and thinks: "Oh, I see, that you are calling from an `Implementation` object, I know it will be the `operator()` in this class scope, I will do this for you".
ROCm, on the other hand, sees your call and thinks “Oh, this is a call to a virtual method, I will look that up for you” - failing to dereference the host function pointer in the host virtual function table.
### How to solve this?
Strictly speaking, the observed behavior on NVCC is an optimization that uses the context information to avoid the V-Table lookup.
If the compiler does not apply this optimization, you can help in different ways by providing additional information. Unfortunately, none of these strategies is fully portable to all backends.
- Tell the compiler not to look up any function name in the V-Table when calling `operator()` by using [qualified name lookup](https://en.cppreference.com/w/cpp/language/qualified_lookup). For this, you tell the compiler which function you want by spelling out the class scope in which the function should be found e.g. `this->Implementation::operator() (i);`. This behavior is specified in the C++ Standard. Nevertheless, some backends are not fully compliant to the Standard.
- Changing the `override` to `final` on the `operator()` in the `Implementation` class. This tells the compiler the `operator()` is not changing in derived objects. Many compilers do use this in optimization and deduce which function to call without the V-Table. Nevertheless, this might only work with certain compilers, as this effect of adding `final` is not specified in the C++ Standard.
- Similarly, the entire derived class `Implementation` can be marked `final`. This is compiler dependent too, for the same reasons.
## Questions/Follow-up
This is intended to be an educational resource for our users. If something doesn't make sense or you have further questions, you'd be doing us a favor by letting us know on [Slack](https://kokkosteam.slack.com) or [GitHub](https://github.com/kokkos/kokkos)
This is intended to be an educational resource for our users. If something doesn't make sense, or you have further questions, you'd be doing us a favor by letting us know on [Slack](https://kokkosteam.slack.com) or [GitHub](https://github.com/kokkos/kokkos)
1 change: 0 additions & 1 deletion docs/source/usecases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Use Cases and Examples
./usecases/MPI-Halo-Exchange
./usecases/Average-To-Nodes
./usecases/MDRangePolicy
./usecases/VirtualFunctions
./usecases/TaggedOperators
./usecases/OverlappingHostAndDeviceWork
./usecases/Tasking
Expand Down
177 changes: 0 additions & 177 deletions docs/source/usecases/VirtualFunctions.md

This file was deleted.

0 comments on commit 666841b

Please sign in to comment.