diff --git a/charm4py/charm.py b/charm4py/charm.py index 46ae416d..c343eb62 100644 --- a/charm4py/charm.py +++ b/charm4py/charm.py @@ -1110,6 +1110,12 @@ def CcsIsRemoteRequest(self): def CcsSendReply(self, message): self.lib.CcsSendReply(message) + def CcsDelayReply(self): + return self.lib.CcsDelayReply() + + def CcsSendDelayedReply(self, d, message): + self.lib.CcsSendDelayedReply(d, message) + def callHandler(self, handlername, data): if handlername in self.ccs_methods: self.ccs_methods[handlername](data) diff --git a/charm4py/charmlib/ccharm.pxd b/charm4py/charmlib/ccharm.pxd index c21a0d38..ab559e29 100644 --- a/charm4py/charmlib/ccharm.pxd +++ b/charm4py/charmlib/ccharm.pxd @@ -105,10 +105,13 @@ cdef extern from "ccs-server.h": ChMessageInt_t len cdef extern from "conv-ccs.h": + ctypedef struct CcsDelayedReply: + CcsImplHeader *hdr; void CcsRegisterHandlerExt(const char *ccs_handlername, void *fn); int CcsIsRemoteRequest(); void CcsSendReply(int replyLen, const void *replyData); - + void CcsSendDelayedReply(CcsDelayedReply d,int replyLen, const void *replyData) + CcsDelayedReply CcsDelayReply() cdef extern from "spanningTree.h": void getPETopoTreeEdges(int pe, int rootPE, int *pes, int numpes, unsigned int bfactor, diff --git a/charm4py/charmlib/charmlib_cython.pyx b/charm4py/charmlib/charmlib_cython.pyx index d4d16b6e..8b2f37d0 100644 --- a/charm4py/charmlib/charmlib_cython.pyx +++ b/charm4py/charmlib/charmlib_cython.pyx @@ -350,9 +350,8 @@ cdef void recvRemoteMessage(void *msg) noexcept: # turn char arrays into strings handler_name = incomingMsgPtr.handler_name[:handler_length].decode('utf-8') - data = incomingMsgPtr.data[:data_length].decode('utf-8') - - charm.callHandler(handler_name, data) + data_bytes = incomingMsgPtr.data[:data_length] + charm.callHandler(handler_name, data_bytes) class CharmLib(object): @@ -909,13 +908,22 @@ class CharmLib(object): def isRemoteRequest(self): return bool(CcsIsRemoteRequest()) - def CcsSendReply(self, str message): - cdef bytes message_bytes = message.encode("utf-8") - cdef const char* replyData = message_bytes - - cdef int replyLen = len(message_bytes) + def CcsSendReply(self, bytes message): + cdef const char* replyData = message + cdef int replyLen = len(message) CcsSendReply(replyLen, replyData) + def CcsDelayReply(self): + cdef CcsDelayedReply* token = malloc(sizeof(CcsDelayedReply)) + token[0] = CcsDelayReply() + return token + + def CcsSendDelayedReply(self, uintptr_t p, bytes msg): + cdef const char* replyData = msg + cdef CcsDelayedReply* token = p + CcsSendDelayedReply(token[0], len(msg), replyData) + free(token) + def hapiAddCudaCallback(self, stream, future): if not HAVE_CUDA_BUILD: raise Charm4PyError("HAPI usage not allowed: Charm++ was not built with CUDA support") diff --git a/charm4py/liveviz.py b/charm4py/liveviz.py new file mode 100644 index 00000000..0ebe7c19 --- /dev/null +++ b/charm4py/liveviz.py @@ -0,0 +1,230 @@ +from .charm import charm, Chare, register +from dataclasses import dataclass, field +from collections import deque +import struct +from itertools import chain +Reducer = charm.reducers + +group = None + +def viz_gather(contribs): + return list(chain(*contribs)) + +def viz_gather_preprocess(data, contributor): + return [data] + +Reducer.addReducer(viz_gather, pre=viz_gather_preprocess) + +@dataclass +class Config: + version: int = 1 + isColor: bool = True + isPush: bool = True + is3d: bool = False + min: tuple = field(default_factory=lambda: (0.0, 0.0, 0.0)) + max: tuple = field(default_factory=lambda: (1.0, 1.0, 1.0)) + + def to_binary(self): + # Format: int, int, int, int, [double, double, double, double, double, double] + binary_data = struct.pack(">iiii", + self.version, + 1 if self.isColor else 0, + 1 if self.isPush else 0, + 1 if self.is3d else 0) + if self.is3d: + binary_data += struct.pack(">dddddd", + self.min[0], self.min[1], self.min[2], + self.max[0], self.max[1], self.max[2]) + return binary_data + +class Vector3d: + def __init__(self, x=0.0, y=0.0, z=0.0): + self.x = x + self.y = y + self.z = z + + @classmethod + def from_bytes(cls, data, offset=0): + # Read 3 doubles from the data starting at offset + x, y, z = struct.unpack_from(">ddd", data, offset) + return cls(x, y, z), offset + 24 # 24 = 3 * 8 bytes (double) + +class ImageRequest: + def __init__(self, version, request_type, width, height, + x=None, y=None, z=None, o=None, minZ=0.0, maxZ=0.0): + self.version = version + self.request_type = request_type + self.width = width + self.height = height + self.x = x + self.y = y + self.z = z + self.o = o + self.minZ = minZ + self.maxZ = maxZ + + @classmethod + def from_bytes(cls, data): + if len(data) < 16: # At least 4 ints + raise ValueError("Not enough data to decode ImageRequest") + + version, request_type, width, height = struct.unpack_from(">iiii", data, 0) + + # If there's more data, we have the optional fields + if len(data) > 16: + offset = 16 + x, offset = Vector3d.from_bytes(data, offset) + y, offset = Vector3d.from_bytes(data, offset) + z, offset = Vector3d.from_bytes(data, offset) + o, offset = Vector3d.from_bytes(data, offset) + minZ, maxZ = struct.unpack_from(">dd", data, offset) + + return cls(version, request_type, width, height, x, y, z, o, minZ, maxZ) + else: + return cls(version, request_type, width, height) + +@register +class LiveVizGroup(Chare): + + def __init__(self, cb, poll): + self.callback = cb + self.poll = poll + charm.CcsRegisterHandler("lvImage", self.image_handler) + if poll: + self.requests = deque() + self.images = deque() + + def send(self, result): + image = ByteImage.from_contributions(result, LiveViz.cfg.isColor) + if self.poll: + if len(self.requests) > 0: + req, delayed = self.requests.popleft() + output = ByteImage.with_image_in_corner(image, req.width, req.height) + charm.CcsSendDelayedReply(delayed, output.to_binary()) + else: + print("sent") + self.images.append(image) + else: + output = ByteImage.with_image_in_corner(image, self.wid, self.ht) + charm.CcsSendDelayedReply(self.reply, output.to_binary()) + + def image_handler(self, msg): + request = ImageRequest.from_bytes(msg) + if self.poll: + if len(self.images) > 0: + output = ByteImage.with_image_in_corner(self.images.popleft(), request.width, request.height) + charm.CcsSendReply(output.to_binary()) + else: + self.requests.append((request, charm.CcsDelayReply())) + else: + self.ht = request.height + self.wid = request.width + self.callback(request) + self.reply = charm.CcsDelayReply() + +class ByteImage: + def __init__(self, data=None, width=0, height=0, is_color=True): + """ + Initialize a byte image + + Args: + data (bytes, optional): Raw image data as bytes, or None to create empty image + width (int): Image width in pixels + height (int): Image height in pixels + is_color (bool): Whether the image is in color (True) or grayscale (False) + """ + self.width = width + self.height = height + self.is_color = is_color + self.bytes_per_pixel = 3 if is_color else 1 + + if data is not None: + self.data = data + else: + self.data = bytes(width * height * self.bytes_per_pixel) + + @classmethod + def from_contributions(cls, contribs, is_color=True): + """ + Create a ByteImage from multiple contributions, positioning each + contribution at the right location. + + Args: + contribs (list): List of tuples with format + (bytes_data, startx, starty, local_height, local_width, total_height, total_width) + is_color (bool): Whether the image is in color + + Returns: + ByteImage: A composite image with all contributions in the right positions + """ + _, _, _, _, _, total_height, total_width = contribs[0] + bytes_per_pixel = 3 if is_color else 1 + + buffer = bytearray(total_width * total_height * bytes_per_pixel) + + for data, startx, starty, local_height, local_width, _, _ in contribs: + for y in range(local_height): + for x in range(local_width): + src_pos = (y * local_width + x) * bytes_per_pixel + dst_pos = ((starty + y) * total_width + (startx + x)) * bytes_per_pixel + + if src_pos + bytes_per_pixel <= len(data): + buffer[dst_pos:dst_pos + bytes_per_pixel] = (buffer[dst_pos:dst_pos + bytes_per_pixel] + data[src_pos:src_pos + bytes_per_pixel]) % 256 + + return cls(bytes(buffer), total_width, total_height, is_color) + + def to_binary(self): + return self.data + + @classmethod + def with_image_in_corner(cls, src_image, new_width, new_height): + """ + Create a new image with specified dimensions and place the source image + in the top left corner. + + Args: + src_image (ByteImage): Source image to place in the corner + new_width (int): Width of the new image + new_height (int): Height of the new image + + Returns: + ByteImage: A new image with the source image in the top left corner + """ + dest_image = cls(None, new_width, new_height, src_image.is_color) + bytes_per_pixel = dest_image.bytes_per_pixel + + buffer = bytearray(new_width * new_height * bytes_per_pixel) + + # Calculate dimensions to copy + copy_width = min(new_width, src_image.width) + copy_height = min(new_height, src_image.height) + + for y in range(copy_height): + for x in range(copy_width): + src_pos = (y * src_image.width + x) * bytes_per_pixel + + dst_pos = (y * new_width + x) * bytes_per_pixel + + if src_pos + bytes_per_pixel <= len(src_image.data): + buffer[dst_pos:dst_pos + bytes_per_pixel] = src_image.data[src_pos:src_pos + bytes_per_pixel] + + return cls(bytes(buffer), new_width, new_height, src_image.is_color) + +class LiveViz: + cfg = None + + @classmethod + def config_handler(cls, msg): + charm.CcsSendReply(cls.cfg.to_binary()) + + @classmethod + def deposit(cls, buffer, elem, x, y, ht, wid, g_ht, g_wid): + elem.reduce(group.send, data=(buffer,x,y,ht,wid,g_ht,g_wid), reducer=Reducer.viz_gather) + + @classmethod + def init(cls, cfg, cb, poll=False): + global group + cls.cfg = cfg + grp = Chare(LiveVizGroup, args=[cb, poll], onPE=0) + charm.thisProxy.updateGlobals({'group': grp}, awaitable=True, module_name='charm4py.liveviz').get() + charm.CcsRegisterHandler("lvConfig", cls.config_handler) diff --git a/docs/index.rst b/docs/index.rst index 328b02c4..1dd10d52 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,6 +40,7 @@ to the largest supercomputers. channels sections pool + liveviz rules gpus diff --git a/docs/liveviz.rst b/docs/liveviz.rst new file mode 100644 index 00000000..d17e7d84 --- /dev/null +++ b/docs/liveviz.rst @@ -0,0 +1,125 @@ +=================== +liveViz +=================== + +Introduction +----- + +If array elements compute a small piece of a large 2D image, then these +image chunks can be combined across processors to form one large image +using the liveViz library. In other words, liveViz provides a way to +reduce 2D-image data, which combines small chunks of images deposited by +chares into one large image. + +This visualization library follows the client server model. The server, +a parallel Charm4py program, does all image assembly, and opens a network +(CCS) socket which clients use to request and download images. The +client is a small Java program. A typical use of this is: + +.. code-block:: bash + + cd charm4py/examples/liveviz + python -m charmrun.start +p4 ++server ++local liveviz.py ++server-port 1234 + ~/ccs_tools/bin/liveViz localhost 1234 + +Use git to obtain a copy of ccs_tools (prior to using liveViz) and build +it by: + +.. code-block:: bash + + cd ccs_tools; + ant; + +How to use liveViz with Charm4py program +--------------------------------------- +A typical program provides a chare array with one entry method with the +following prototype: + +.. code-block:: Python + + def functionName(self, request) + +This entry method is supposed to deposit its (array element’s) chunk of +the image. This entry method has following structure: + +.. code-block:: Python + + def functionName(self, request): + #prepare image chunk + + liveviz.LiveViz.deposit(data, self, start_x, start_y, local_height, local_width, global_height, global_width) + +Here, “local_width” and “local_height” are the size, in pixels, of this array +element’s portion of the image, contributed in data (described +below). This will show up on the client’s assembled image at 0-based +pixel (start_x,start_y). LiveViz combines image chunks by doing a saturating sum of +overlapping pixel values. + +Format of deposit image +----------------------- + +“data” is run of bytes representing a rectangular portion of the +image. This buffer represents image using a row-major format, so 0-based +pixel (x,y) (x increasing to the right, y increasing downward in typical +graphics fashion) is stored at array offset “x+y*width”. + +If the image is gray-scale (as determined by liveVizConfig, below), each +pixel is represented by one byte. If the image is color, each pixel is +represented by 3 consecutive bytes representing red, green, and blue +intensity. + +liveViz Initialization +---------------------- + +liveViz library needs to be initialized before it can be used for +visualization. For initialization follow the following steps from your +main chare: + +#. Create your chare array (array proxy object ’a’) with the entry + method ’functionName’ (described above). + +#. Create a Config object (’cfg’). Config takes a number + of parameters, as described below. + +#. Call liveviz.LiveViz.init(cfg, a.functionName). + +The liveviz.Config parameters are: + +- isColor, where “False” means a greyscale image (1 + byte per pixel) and “True” means a color image (3 RGB + bytes per pixel). This defaults to True. + +- The second parameter is the flag "isPush", which is passed to the + client application. If set to True, the client will repeatedly + request for images. When set to False the client will only request + for images when its window is resized and needs to be updated. This also defaults to True. + +Poll Mode +--------- + +In some cases you may want a server to deposit images only when it is +ready to do so. For this case the server will not register a callback +function that triggers image generation, but rather the server will +deposit an image at its convenience. For example a server may want to +create a movie or series of images corresponding to some timesteps in a +simulation. The server will have a timestep loop in which an array +computes some data for a timestep. At the end of each iteration the +server will deposit the image. The use of LiveViz’s Poll Mode supports +this type of server generation of images. To use poll mode, simply set +poll = True during initialization. + +.. code-block:: Python + + liveviz.LiveViz.init(cfg, a.functionName, poll=True) + +To deposit an image, the server just calls liveVizDeposit. The +server must take care not to generate too many images, before a client +requests them. Each server generated image is buffered until the client +can get the image. The buffered images will be stored in memory on +processor 0. + +Sample liveViz and liveVizPoll servers are available at: + +.. code-block:: none + + .../charm4py/examples/liveviz \ No newline at end of file diff --git a/examples/ccs/ccs_server.py b/examples/ccs/ccs_server.py index 7144c54b..5739949c 100644 --- a/examples/ccs/ccs_server.py +++ b/examples/ccs/ccs_server.py @@ -2,11 +2,11 @@ def handler(msg): print("CCS Ping handler called on " + str(charm.myPe())) - - msg = msg.rstrip('\x00') #removes null characters from the end + msg = msg.decode('utf-8') + msg = msg.rstrip('\x00') answer = "Hello to sender " + str(msg) + " from PE " + str(charm.myPe()) + ".\n" - # send the answer back to the client - charm.CcsSendReply(answer) + answer_bytes = answer.encode('utf-8') + charm.CcsSendReply(answer_bytes) class RegisterPerChare(Chare): diff --git a/examples/liveviz/README.md b/examples/liveviz/README.md new file mode 100644 index 00000000..af9f0b8c --- /dev/null +++ b/examples/liveviz/README.md @@ -0,0 +1,43 @@ +## Instructions for Running the LiveViz Example + +### Step 1: Set Up Python Environment + +Activate your Python virtual environment if you're using one + +### Step 2: Launch LiveViz Server + +Start the server with the following command, replacing `` with the number of processors and `` with your desired port: + +```bash +python -m charmrun.start +p ++server ++local liveviz.py ++server-port +``` + +### Step 3: Capture Server Details + +After running the command above, note down the displayed **Server IP** and **Server Port** (the port will match the one specified earlier if you used `++server-port`). + +### Step 4: Clone and Build CCS Tools + +Clone the `ccs_tools` repository: + +```bash +git clone https://github.com/UIUC-PPL/ccs_tools.git +cd ccs_tools +ant +``` + +### Step 5: Run the LiveViz Client + +Launch the client application using the captured server details: + +```bash +./liveViz +``` + +### Step 6: Verify Visualization + +The LiveViz client will connect to the server and display a live visualization of the transmitted data, similar to the image below: + +![Visualization Example](image-1.png) + +Ensure your setup matches the displayed output. diff --git a/examples/liveviz/image-1.png b/examples/liveviz/image-1.png new file mode 100644 index 00000000..da872e56 Binary files /dev/null and b/examples/liveviz/image-1.png differ diff --git a/examples/liveviz/liveviz.py b/examples/liveviz/liveviz.py new file mode 100644 index 00000000..c1538f02 --- /dev/null +++ b/examples/liveviz/liveviz.py @@ -0,0 +1,36 @@ +from charm4py import charm, Chare, Array, Future, Reducer, Group, liveviz, coro +import random + +class Unit(Chare): + + def __init__(self): + self.colors = [(200, 0, 0), (0, 200, 0), (0, 0, 200)] + + def reqImg(self, request): + self.particles = [] + + for _ in range(300): + x = random.randint(0, 49) + y = random.randint(0, 49) + + color = random.choice(self.colors) + + self.particles.append((x, y, color)) + + data = bytearray(50 * 50 * 3) + + for x, y, (r, g, b) in self.particles: + pixel_index = (y * 50 + x) * 3 + data[pixel_index] = r + data[pixel_index + 1] = g + data[pixel_index + 2] = b + + liveviz.LiveViz.deposit(data, self, self.thisIndex[0]*50, self.thisIndex[1]*50, 50, 50, 800, 800) + +def main(args): + units = Array(Unit, dims=(16,16)) + config = liveviz.Config() + liveviz.LiveViz.init(config, units.reqImg) + print("CCS Handlers registered . Waiting for net requests...") + +charm.start(main) diff --git a/examples/liveviz/liveviz_poll.py b/examples/liveviz/liveviz_poll.py new file mode 100644 index 00000000..ed1ea873 --- /dev/null +++ b/examples/liveviz/liveviz_poll.py @@ -0,0 +1,39 @@ +from charm4py import charm, Chare, Array, Future, Reducer, Group, liveviz, coro +import time +import random + +class Unit(Chare): + + def __init__(self): + self.colors = [(200, 0, 0), (0, 200, 0), (0, 0, 200)] + + def reqImg(self): + for i in range(50): + self.particles = [] + + for _ in range(300): + x = random.randint(0, 49) + y = random.randint(0, 49) + + color = random.choice(self.colors) + + self.particles.append((x, y, color)) + + data = bytearray(50 * 50 * 3) + + for x, y, (r, g, b) in self.particles: + pixel_index = (y * 50 + x) * 3 + data[pixel_index] = r + data[pixel_index + 1] = g + data[pixel_index + 2] = b + + liveviz.LiveViz.deposit(data, self, self.thisIndex[0]*50, self.thisIndex[1]*50, 50, 50, 800, 800) + +def main(args): + units = Array(Unit, dims=(16,16)) + config = liveviz.Config() + liveviz.LiveViz.init(config, units.reqImg, poll=True) + units.reqImg() + print("CCS Handlers registered . Waiting for net requests...") + +charm.start(main)