LLM 101 Introducción

Vamos a hacer una serie de preguntas rápidas para ayudar a los alumnos a empezar a trabajar con modelos grandes del lenguaje o LLM (Large Language Model)

¿Que es un modelo grande del lenguaje o LLM del inglés Large Language Model?

Es una red neuronal profunda (con muchas capas) diseñadas para procesar lenguaje natural (NLP del inglés natural language processing). Esto es, entender y generar lenguaje natural escrito. Como todas las redes neuronales, hay que entrenarlas. Una LLM se entrena con mucho texto y “aprende” patrones complejos del lenguaje.

¿Por qué se han puesto de moda?

Hace unos años, unas arquitecturas llamadas Transformer y sobre todo, el entrenamiento masivo con toda la información que hay disponible en Internet ha hecho que los LLM tengan unas prestaciones increíbles en tareas concretas, mas allá de las que originalmente fueron diseñadas. chatGPT (Generative Pre-trained Transformer) de OpenAI y BERT (Bidrectional Encoder Representations form Transformer) de google son las arquitecturas mas famosas. Una descripción de la arquitectura transformer puedes verla aquí

¿Pero un LLM es un programa que ejecuto en mi ordenador?

No exactamente, es un archivo binario que contiene la configuración de la red neuronal después del entrenamiento, esto es, los “pesos” de cada neurona, de cada capa, calculados en el proceso de aprendizaje.
Los modelos pueden tener distintos formatos, generalmente indicados en el nombre. Aunque no hay un estándar de nombrado, se detectan patrones en el nombrado de los modelos. Por ejemplo, SynthIA-7B-v2.0-16k-AWQ es un modelo entrenado con 7B, siete billones de parámetros, en su versión 2.0 y con un tamaño de contexto de 16K. La AWQ indica el formato y la precisión en el cual se han almacenado los “pesos” de cada neurona.

¿Qué es el tamaño del contexto?

Una cosa es el modelo LLM y otra cosa son sus parámetros. El tamaño del contexto es el número máximo de tokens (palabras) que le puedes pasar al LLM. Lo define la arquitectura del LLM y generalmente, a mayor tamaño del contexto, mejor se puede especificar la tarea que le pides al LLM por lo que generará cosas mas precisas. También es cierto que generalmente, implica mas memoria. Es el prompt que le metes a chatGPT para que te responda.

Vale, pero ¿Cómo ejecuto un LLM en mi máquina?

Bien, tienes que cargar el modelo binario en memoria para poder introducirles contexto (el famoso prompt) y obtener lo que genera. Los modelos pueden ser muy grandes por lo que los requisitos hardware son exigentes. Dicho esto, hay librerías en varios lenguajes de programación que te permiten cargar en memoria e interactuar con LLMs mediante un sencillo programa.
Uno de los mejores recursos para trabajar en este campo y en IA en general es la web https:/huggingface.co , que tiene librerías, modelos, datasets, papers, etc. la guía de uso de esta página es un buen primer comienzo.

¿Me puedes poner un ejemplo?

El código python de mas abajo carga un modelo un modelo LLM de código abierto modesto, que se baja de la web https://huggingface.co/ e interacciona con él introduciéndoles prompts (el contexto que hablábamos antes) y obteniendo las respuestas. Realmente trata de predecir la siguiente palabra al prompt.

import time
import torch
import transformers
import accelerate

from transformers import AutoTokenizer, AutoModelForCausalLM

print("Loading model...")
print("transformer version: ", transformers.__version__)
print("accelerate version: ", accelerate.__version__)
timeStart = time.time()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print("Using device:", device)

hugginfacemodelname = "openchat/openchat-3.5-1210"
tokenizer = AutoTokenizer.from_pretrained(hugginfacemodelname)
model = AutoModelForCausalLM.from_pretrained(
    hugginfacemodelname,
    torch_dtype=torch.bfloat16,
    low_cpu_mem_usage=True,
).to(device)

print("Load model time: ", -timeStart + time.time())

while True:
    input_str = input("Enter: ")
    input_token_length = input("Enter length: ")
    if input_str == "exit":
        break
    timeStart = time.time()
    inputs = tokenizer.encode(input_str, return_tensors="pt").to(device)
    outputs = model.generate(
        inputs,
        max_new_tokens=int(input_token_length),
    )
    output_str = tokenizer.decode(outputs[0])
    print(output_str)
    print("Time taken: ", -timeStart + time.time())

El código utiliza la librería python transformers y torch. En primer lugar se indica el modelo LLM a cargar, openchat/openchat-3.5-1210 y se genera el tokenizer que no es sino una herramienta que convierte el texto en tokens (palabras). Para el modelo que hemos especificado, la función “from_pretained” busca y carga el tokenizador correspondiente. Si se lo tiene que bajar, esto puede tardar. A continuación, con AutoModelForCausalLM.from_pretained.., se carga el modelo de lenguaje causal, un tipo de modelo que predice la siguiente palabra en una secuencia.

¿Hay mas tipos de modelos?

Si, en esa librería hay modelos específicos para pregunta-respuesta, para clasificación de imágenes, multimodales, etc. al cual le tienes que indicar qué modelo quieres entrenar.

¿El prompt o la entrada del LLM cómo se articula?

Asociado al concepto de LLM aparece el concepto de prompt engineering o ingeniería de prompts. Es importante destacar que un buen prompt o cadena de prompts puede mejorar mucho la salida de una determinada LLM en una tarea específica. En los últimos años aparece una gran cantidad de métodos para elaborar prompts:
Referencias: https://dl.acm.org/doi/pdf/10.1145/3560815

¿ Puedo entrenar un LLM en lo que yo quiera?

En principio si. Entrenar un LLM desde cero requiere de muchos recursos. La mayoría de los ingenieros hacen un proceso denominado finetunning que no es otra cosa que coger un modelo LLM ya entrado de forma genérica y aplicarles un proceso de entrenamiento con un conjunto de datos específico de la tarea donde quiero usar mi LLM. El resultado es un modelo LLM que tiene el conocimiento general y se ha especializado mediante lo que ha aprendido con los patrones del dataset con el cual has hecho el finetunning. Un ejemplo muy bueno que aparece en los vídeos de formación de google relacionados con LLM, es ver el finetunning similar al proceso de enseñarle a una mascota, por ejemplo un perro, una serie de tareas específicas (buscar droga, personas, órdenes, etc.). Tienes el perro, siendo en este simil, la LLM generalista, y mediante un proceso de entrenamiento (finetunning), obtienes una LLM que es mejor que la generalista en un proceso específico.

Otra técnica muy adecuada es el Retrieval Augmented Generation o RAG, que no es sino mejorar las respuestas de LLM en un dominio específico proporcionándole información específica de ese dominio. Lo que se hace es recuperar información relevante (de fuentes internas o externas) para la pregunta e introducirla en el contexto/prompt junto con la pregunta para obtener mejores respuestas. Realmente todo este proceso de recuperación lo que hace es mejorar el prompt.

Finetunning

En este enlace podemos ver los principios básicos de finetunning:
hay tres aproximaciones al finetuning

  1. Self-supervised learning
  2. Supervised learning
  3. Reinforcement learning

¿Por qué ocupa tanto una LLM?

Una red neuronal almacena pesos calculados en el proceso de entrenamiento, como toda
variable, esos pesos pueden almacenarse en diverso formatos (punto flotante vs entero) y con determinada precisión (64,32,16 bits). La precisión es importante por que es lo que almacena los patrones aprendidos.

¿Qué puedo hacer para reducir el tamaño de una LLM?

Hay un proceso, denominado cuantización, que pasa de una determinada precisión a otra. Si pasas de una precisión en punto flotante de 16 bits a un entero de 8 bits, esto hace que el modelo ocupe menos en el disco duro a costa de perder información/patrones. Es decir, el modelo es algo peor. Hay dos tipos de cuantización, simétrica y asimétrica Mas info: https://myscale.com/blog/es/quantization-for-llms-finetuning/

En el siguiente ejemplo podemos ver un ejemplo básico de finetunning para evaluar sentimiento en comentarios de películas. El ejemplo es muy sencillo pero completo, en primer lugar se evalúa con un pequeño testbench la LLM escogida, una de las mas sencillas, se entrena con un dataset de comentarios etiquetados y se vuelve a evaluar para ver si ha “aprendido”. https://towardsdatascience.com/fine-tuning-large-language-models-llms-23473d763b91

¿Cómo evaluamos un LLM?

Cómo sabemos si una LLM es buena o malo y qué podemos probar para ver si hemos mejorado. Bueno, aquí entra en juego la evaluación de las redes neuronales en general. Es decir, evaluar las prestaciones en base a una tarea específica pasa inherentemente por poder evaluar cómo de bueno son los resultados obtenidos ante prompts especificos.

Dos paradigmas populares, para la evaluación genérica de LLMs:

  1. El tradicional NLP Benchmark approach usando, por ejemplo, MosaicML’s Eval Gauntlet.
  2. Otra técnica muy utilizada es , precisamente, utilizar una LLM como juez.

¿Qué LLM es mejor?

Con la aparición de múltiples LLMs han aparecido numerosas métricas y rankings en diferentes testbench. Aquí puedes ver el ranking de hugging face: link

Herramientas y recursos

https://pinokio.computer/ una WEBUI para instalar y lidiar con modelos de AI.
https://huggingface.co/TheBloke Distintos modelos y parámetros para poder ser ejecutados en máquinas mas modestas
https://github.com/jmorganca/ollama herramienta para ejecutar LLM de forma local y hacerle un wrapper python que te permita adaptar los prompts/salidas.

Terminología:

Cuantización: Reduce la precisión de los números utilizados para representar los pesos en el modelo, pierdes precisión pero el tamaño y la inferencia se acelera sin necesitar tantos recursos.
GPTQ : formato de cuantización
-AWQ : formato de cuantización
-GGUF : formato de modelos que reemplaza a GGML.
Tamaño del contexto: la cantidad de tokens (normalmente palabras ) que un modelo puede considerar en un prompt de una vez cuando genera una respuesta o predicción.
Formatos de los modelos models in open AI book
– Destilación (Distillation) es el proceso de entrenar un modelo pequeño (llamado estudiante) para emular las respuestas de un modelo mas grande (profesor).
-Pruning es el proceso de elminar las salidas no críticas del modelo pequeño o estudiante.
-Quantization es el proceso de reducir la precisión de los pesos del modelo y las activaciones.

Bibliografía

Open source LLM
XGen-7B Technical Report un LLM open source con un prompt de hasta 8K

Emulación en NS3

El proceso de simulación en el simulador NS3 es el más ampliamente utilizado. Ya lo hemos visto en entradas anteriores. Una característica menos conocida y menos utilizada que te permite NS3 es la emulación. Efectivamente, NS3 te permite crear un escenario (nodos, pila de protocolos y aplicaciones) y que el tráfico generado por ese escenario “salga” al mundo real. Por ejemplo, podemos crear un sensor en ns3 que genere tráfico de forma que el servidor que recoge la información no distinga si es un sensor real o no. Las aplicaciones de la emulación son muy numerosas, por ejemplo, para testear servicios software reales , probar configuraciones, crear perfiles de tráfico, etc.

Vamos a ver un ejemplo de cómo crear este tipo de escenarios, y luego iremos viendo cosas concretas. Nuestro objetivo es generar tráfico LoraWan de un nodo a una pasarela en NS3 y que ese tráfico salga por una interfaz virtual que se crea en el ordenador que ejecuta la simulación.

La parte de la simulación LoraWan ya la hemos visto y tenemos un ejemplo en los repositorios de este blog. Sigue las instrucciones para hacer el ejemplo si no lo has hecho todavía.

Vamos a partir de ese ejemplo de LoraWan básico que constaba de un nodo lora representando un sensor y una pasarela o hub que recibía ese tráfico.

Bien, vamos a coger ese ejemplo y vamos a modificarlo para que ese tráfico lora salga por una interfaz de red virtual que se crea en el ordenador que ejecuta la simulación. Para ello necesitamos dos cosas, una es añadir un nodo que denominaremos “fantasma” que recoja el tráfico de la red que llega a la pasarela LoraWan y la saque por la interfaz virtual. El otro elemento que necesitamos es una aplicación que reciba el tráfico de la interfaz LoRa y lo reenvíe por una interfaz CSMA (Ethernet). Esa aplicación la instalaremos en la pasarela LoraWan y le añadiremos una interfaz CSMA. Al otro extremo de la interfaz CSMA conectaremos el nodo ghost que será al que tendremos que conectar la interfaz virtual del tipo “TapBridge”.

Para ello, creamos una interfaz tapBridge, con su asistente:

TapBridgeHelper tapBridge;
tapBridge.SetAttribute ("Mode", StringValue ("ConfigureLocal"));
tapBridge.SetAttribute ("DeviceName", StringValue ("virtualtwin"));

Ahora vamos a crear un asistente para crear una red Ethernet que conecte el Ghostnode, y el hub que recoge el tráfico LoraWan, éste ghostnode será el que cree la interfaz virtual. Podríamos hacer que el hub que recoge el tráfico LoraWan creara la interfaz virtual, pero realmente los gateways/hub realmente recogen el tráfico y lo reenvian a una interfaz Ethernet y queríamos modelar también ese paso:


NodeContainer ghostNode;
ghostNode.Create(1);
ghostNode.Add(hub.Get(0));
CsmaHelper csma;
csma.SetChannelAttribute ("DataRate", StringValue ("100Mbps"));
csma.SetChannelAttribute ("Delay", TimeValue (NanoSeconds (6560)));

NetDeviceContainer hubDevice = csma.Install(ghostNode);

Básicamente se añade el hub (que ya lo habíamos creado previamente) al nuevo contenedor de nodos y se conectan todos a un canal csma. La expresión hub.Get(0) devuelve el nodo en la posición 0 del asistente de creador de nodos o contenedor de nodos, hub. Ese contenedor sólo tenía un nodo.

Asignamos la dirección IP en ese canal:

InternetStackHelper stack;
stack.Install (ghostNode);
Ipv4AddressHelper address;
address.SetBase ("10.0.0.0", "255.255.255.0");
Ipv4InterfaceContainer p2pInterfaces;
p2pInterfaces = address.Assign(hubDevice);

tapBridge.Install(hub.Get(0),hubDevice.Get(0));

y ya solo nos falta instalar las aplicaciones pertinentes, la primera es la aplicación que simulará el tráfico del sensor, ya la hemos visto:

PeriodicSenderHelper periodicSenderHelper;
periodicSenderHelper.SetPeriod (Seconds (12));
periodicSenderHelper.Install (nodes);

También necesitamos una aplicación que recoja toda la información que le llega a la interfaz lorawan del hub y la reenvíe por la interfaz csma que le hemos añadido. Por defecto, esa aplicación no existe, por lo que la hemos tenido que crear junto con su asistente para instalarlo. En otra entrada de blog la analizaremos, por el momento vamos a ver que se “instala” en el hub como otra aplicación cualquiera:

ForwardercdmaHelper ForwardercdmaHelper;
ForwardercdmaHelper.Install (hub);

Hemos incluido el código de esa aplicación (ForwaredercdmaHelper) en el ejemplo de esta entrada.

Al ejecutar la simulación podemos ver cómo se genera una interfaz “virtualtwin” mediante el comando ifconfig.

Created with GIMP

En esa interfaz sale todo el tráfico que mandemos al nodo ghost, entre ellos, el tráfico lorawan que hemos reenviado en el hub.

El ejemplo completo lo podemos encontrar en el directorio emulación del repositorio github

Simulando consumo de energía en NS3 (II)

Seguimos explicando el modelo de simulación del consumo de energía. Ya explicamos en la primera entrada relativa a la energía los conceptos básicos modelados en NS3. Seguimos trabajando con el ejemplo básico examples/energy/energy-model-example.cc

Si observamos el ejemplo, existen dos funciones añadidas justo antes de la función principal, RemainingEnergy y TotalEnergy:

/// Trace function for remaining energy at node.
void
RemainingEnergy (double oldValue, double remainingEnergy)
{
NS_LOG_UNCOND (Simulator::Now ().GetSeconds ()
<< "s Current remaining energy = " << remainingEnergy << "J");
}

/// Trace function for total energy consumption at node.
void
TotalEnergy (double oldValue, double totalEnergy)
{
NS_LOG_UNCOND (Simulator::Now ().GetSeconds ()
<< "s Total energy consumed by radio = " << totalEnergy << "J");
}

Estas funciones, como podemos ver, imprimen la energía que queda en la fuente de energía y el total de energía consumida por la radio.

Para invocar estas funciones cuando existe un cambio en la energía restante y/o energía consumida, debemos enlazar las funciones anteriores a los cambios de estado de la fuente de energía y del modelo de energía consumido respectivamente. De esta forma, obtenemos primero una referencia a la fuente de energía con ID 1. A continuación, mediante la función TraceConnectWithoutContext, vinculamos el cambio de estado en RemainingEnergy a una llamada a la misma función, mediante MakeCallBack y un puntero a la función deseada, en este caso RemainingEnergy también:

Ptr basicSourcePtr = DynamicCast (sources.Get (1));
basicSourcePtr->TraceConnectWithoutContext ("RemainingEnergy", MakeCallback (&RemainingEnergy));

A continuación se sigue el mismo proceso obteniendo el modelo de consumo de energía de un dispositivo:

// device energy model
Ptr basicRadioModelPtr =
basicSourcePtr->FindDeviceEnergyModels ("ns3::WifiRadioEnergyModel").Get (0);
NS_ASSERT (basicRadioModelPtr != NULL);
basicRadioModelPtr->TraceConnectWithoutContext ("TotalEnergyConsumption", MakeCallback (&TotalEnergy));

De esta forma se tracean los cambios de estado en las fuentes de energía, que simulan baterías, modelos de energía, que simulan el consumo de las tarjetas de red, y los recolectores de energía que simularían paneles solares, viento, etc.

La función TraceconnectWithoutContext conecta una fuente de trazas (es decir, algo que representa un estado dentro de NS3 y que puede cambiar) con una función. Identificaremos estas fuentes de trazas por que son variables de la plantilla TracedValue, por ejemplo, puedes ver en la definición BasicEnergySource tiene una variable TracedValue m_remainingEnergyJ; que es susceptible de ser traceada mediante una función asociada con TraceconnectWithoutContext asocíandole una función que tenga como argumentos el valor antiguo y nuevo. El tipo de argumentos en este caso será double. La documentación de cada clase proporciona qué TracedSources tiene para usar este mecanismo (e.g. BasicEnergySource).

Argumentos de entrada en simulaciones de NS3

Para ahorrarte trabajo, puedes configurar mediante argumentos de entrada muchos parámetros o atributos de tu simulación, tanto de los módulos que usas de NS3, como definir tus propios parámetros. Por ejemplo, si vas a simular el tráfico en una red lorawan con topología en estrella, con la pasarela en el medio, puedes poner como parámetro configurable el número de nodos o el radio de la estrella. Este ejemplo es el que vamos a utilizar para explicar cómo gestionar los argumentos de entrada. El archivo que usaremos es el lorawan.cc del repositorio. Necesitarás instalar el módulo de lorawan como indicamos aquí

Si copiamos el archivo lorawan.cc al directorio scratch de nuestra instalación NS3 podemos ver qué argumentos podemos definir por la linea de comandos mediante la opción –PrintHelp, esto es:

$ ./waf --run "lorawan --PrintHelp"
lorawan [Program Options] [General Arguments]
[...]
Program Options:
--radio: Radio of the disc where random put the nodes [20]
--numnodes: Num. nodes in the grid for simulating [20]


General Arguments:
--PrintGlobals: Print the list of globals.
--PrintGroups: Print the list of groups.
--PrintGroup=[group]: Print all TypeIds of group.
--PrintTypeIds: Print all TypeIds.
--PrintAttributes=[typeid]: Print all attributes of typeid.
--PrintHelp: Print this help message.

Como podemos ver, después de los mensajes de compilación (sustituidos por […]), podemos ver que el programa lorawan tiene definidos dos opciones, el radio y el numnodes, indicando el radio de la estrella y el número de nodos respectivamente. El número por defecto en ambos casos y si no indicamos esas opciones es 20. Si queremos simular 3 nodos en una topología en estrella con un radio de 10 metros, solo tendremos que indicarlo en línea de argumentos:
$ ./waf --run "lorawan --numnodes=3 --radio=10"
[...]

Si analizamos el archivo lorawan.cc, la definición de argumentos es relativamente sencilla, en primer lugar se indica que vamos a parsear la línea de comandos en busca de atributos y se definen las variables que almacenarán los valores que se pasen por línea de comandos:

#include "ns3/command-line.h"
[...]
double radio = 20.0;
int numnodes = 20;

A continuación se definen los valores esperados en línea de argumentos, en nuestro ejemplo, lo hacemos mediante una función que definimos antes del main y añadimos al final. En esta función creamos la variable que almacenará y parseará los argumentos y añadimos nuestros dos argumentos esperados mediante AddValue:

CommandLine setupcmd(){
CommandLine cmd;
cmd.AddValue ("radio", "Radio of the disc where random put the nodes", radio);
cmd.AddValue ("numnodes", "Num. nodes in the grid for simulating",numnodes);
return cmd;
}

como puedes ver indicamos el nombre, una pequeña ayuda y las variables que van a almacenar los valores.
Finalmente, parseamos la línea de comandos:

[..]
CommandLine cmd =setupcmd();
cmd.Parse (argc, argv);
[..]

Con lo que en las variables definidas a tal efecto, guardaremos los valores que le pasemos.

Aparte de poder definir nuestros propios argumentos de entrada para configurar nuestras simulaciones, muchos módulos que usamos tienen sus propios atributos configurables por líneas de comandos. Por ejemplo, si queremos ver un listado de todos los módulos disponibles, podemos verlos con la opción –PrintTypeIds

./waf --run "lorawan --PrintTypeIds"
Waf: Entering directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Waf: Leaving directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.112s)
Registered TypeIds:
ns3::A2A4RsrqHandoverAlgorithm
ns3::A3RsrpHandoverAlgorithm
[..]
ns3::LoraChannel
ns3::LoraInterferenceHelper
ns3::LoraNetDevice
ns3::LoraPhy
ns3::LoraRadioEnergyModel
ns3::LoraTag
ns3::LoraTxCurrentModel
[...]

En esa larga lista, podemos explorar qué atributos son configurables mediante la opción –PrintAttributes y el identificador o nombre del módulo. Por ejemplo, si queremos saber qué atributos puedo especificar mediante línea de comandos del módulo de ns3 LoraRadioEnergyModel, podemos ejecutar:

$ ./waf --run "lorawan --PrintAttributes=ns3::LoraRadioEnergyModel"
Waf: Entering directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Waf: Leaving directory `/home/felix/tools/ns-allinone-3.30/ns-3.30/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.063s)
Attributes for TypeId ns3::LoraRadioEnergyModel
--ns3::LoraRadioEnergyModel::RxCurrentA=[0.0112]
The radio Rx current in Ampere.
--ns3::LoraRadioEnergyModel::SleepCurrentA=[1.5e-06]
The radio Sleep current in Ampere.
--ns3::LoraRadioEnergyModel::StandbyCurrentA=[0.0014]
The default radio Standby current in Ampere.
--ns3::LoraRadioEnergyModel::TxCurrentA=[0.028]
The radio Tx current in Ampere.
--ns3::LoraRadioEnergyModel::TxCurrentModel=[0]
A pointer to the attached tx current model.



Donde podemos ver que podemos definir la corriente de envío, recepción, dormir, etc.

Instalar el módulo LoRaWAN en NS3

[Adaptado a la versión 3.36 con cmake]

LoRaWAN es una tecnología especialmente diseñada para IoT en dispositivos alimentados por baterías, con necesidades de comunicación bidireccional y un ancho de banda limitado (entre 0.3kbps y 50kbps). La topología por excelencia es la de estrella, con la pasarela en el centro de la estrella (que conecta la red LoRaWAN a una red IP convencional).

Existen tres clases de dispositivos en LoRaWAN (clase A, B, C) asociados a tres funcionalidades muy concretas. La clase A está asociado a un dispositivo final que periódicamente o bajo requerimientos de la aplicación, manda la información a la pasarela y espera una ventana de tiempo por si hubiera algún tipo de necesidad de comunicación en el sentido pasarela->dispositivo. Este esquema permite dormir el dispositivo de clase A entre transmisiones y obliga a guardar todas las comunicaciones en el sentido pasarela-> dispositivos y esperar a que se produzca una comunicación en el sentido dispositivo -> pasarela.

La clase B se sincroniza con la red y habilita un periodo de escucha de forma periódica (ventana máxima de 128 segundos) por si hubiera un mensaje hacia el dispositivo. De esta forma se puede hacer determinista cuanto tiempo tarda en recibir un mensaje de la red enviado por la pasarela. No obstante este despertar periódico consume batería y los hace menos eficiente que la clase A desde el punto de vista del consumo energético.

Finalmente, la clase C está siempre escuchando por lo que cualquier transmisión desde la red (pasarela) es inmediatamente recibido a menos que el propio dispositivo esté transmitiendo. Se pueden intercambiar los modos de funcionamiento entre la clase A y la clase C si necesitamos, por ejemplo, enviar una actualización de firmware a través de la interfaz inalámbrica.
En NS3 hay varias implementaciones de un módulo de NS3 para simular la tecnología LoRaWAN. El mas estable está disponible en la App Store de NS3.
La instalación es sencilla, pasa por bajarse el código en el directorio src de la instalación NS3 con git clone y la dirección https://github.com/signetlabdei/lorawan, y volver a configurar y compilar todo:

$ns-allinone-3.36/ns-3.36/src$ git clone https://github.com/signetlabdei/lorawan
Clonando en 'lorawan'...
remote: Enumerating objects: 37, done.
..
..
Resolviendo deltas: 100% (906/906), listo.

A continuación, en el directorio principal, la configuración y compilación habitual, habilitando los test y los ejemplos:

$ns-allinone-3.36/ns-3.36/$./ns3 configure --enable-test --enable-examples
..
..

a la hora de construir, fijarnos que el módulo lorawan aparece en los módulos compilados:

$/ns-allinone-3.36/ns-3.36$ ./ns3 build

Para comprobar que todo ha ido bien, podemos ejecutar sus test específicos del módulo:

$/ns-allinone-3.36/ns-3.36$./test.py -s lorawan
.....
[1/1] PASS: TestSuite lorawan
1 of 1 tests passed (1 passed, 0 skipped, 0 failed, 0 crashed, 0 valgrind errors)

y el ejemplo básico:

/ns-allinone-3.36/ns-3.36$ ./ns3 run simple-network-example
SimpleLorawanNetworkExample:main(): Creating the channel...
+0.000000000s -1 SimpleLorawanNetworkExample:main(): Setting up helpers...
+0.000000000s -1 LoraPhyHelper:LoraPhyHelper(0x7fff858a3a70)
+0.000000000s -1 SimpleLorawanNetworkExample:main(): Creating the end device...
+0.000000000s -1 LoraPhyHelper:SetDeviceType(0x7fff858a3a70, 1)
+0.000000000s -1 LorawanMacHelper:SetDeviceType(0x7fff858a3aa0, 1)
+0.000000000s -1 LoraHelper:Install()
+0.000000000s -1 LoraPhyHelper:Create(0x7fff858a3a70, 0, 0x5617411f0970)
+0.000000000s -1 LoraInterferenceHelper:LoraInterferenceHelper(0x5617411f0bf8)
+0.000000000s -1 LoraInterferenceHelper:SetCollisionMatrix(): Setting the GOURSAUD collision matrix
+0.000000000s -1 LoraPhy:SetChannel(0x5617411f0bc0, 0x5617411f0090)
+0.000000000s -1 LoraPhy:SetDevice(0x5617411f0bc0, 0x5617411f0970)
+0.000000000s -1 LoraHelper:Install(): Done creating the PHY
....

También hay un ejemplo de energía, que genera un archivo de texto battery-level.txt que guarda el nivel de batería para un dispositivo de clase A, a lo largo de una simulación:

/ns-allinone-3.36/ns-3.36$ ./ns3 run energy-model-example
+0.000000000s -1 Assign IP Addresses.
0.000574667s Current remaining energy = 0.0995293J
0.000574667s Total energy consumed by radio = 0.000470652J
0.000714667s Current remaining energy = 0.0994147J
0.000714667s Total energy consumed by radio = 0.000585312J
0.000762667s Current remaining energy = 0.0993754J
0.000762667s Total energy consumed by radio = 0.000624624J
0.00287467s Current remaining energy = 0.0973922J
0.00287467s Total energy consumed by radio = 0.00260779J
.....

En futuras entradas, utilizaremos esta tecnología para ir creando una serie de escenarios de IoT con los que poder estudiar aspectos concretos.

Ejemplo básico NS3: IoT y sensores

Como ya hemos comentado en las primeras secciones, hay numerosos ejemplos y test que te sirven de ayuda y como punto de partida, vamos a realizar un ejemplo básico intentando simular una red de sensores que mandan información a una pasarela (Gateway) o nodo central. El objetivo de este ejemplo es ir completándolo con otras funciones muy necesarias en simulación (Log, trazas, configuración, varias tecnologías, energía, etc.) en entradas posteriores. En el repositorio del tutorial, el archivo de inicio es basic-iot-sensors.cc.
Lo primero que vamos a realizar es crear 3 nodos (variable numsensors) y posicionarlos en un punto determinado (aleatorio). Utilizamos una función que crea un escenario con un radio (variable radio) determinado en metros y, a continuación, le pasamos el contenedor de nuestros nodos para que posicione, de forma aleatoria, dichos nodos;

NodeContainer nodes;
nodes.Create(numsensors);
MobilityHelper scenario = createscenario(radio);
scenario.Install (nodes);

Como ya indicamos en la entrada de topología , se crea un escenario mediante el MobilityHelper con la configuración deseada:

MobilityHelper createscenario(double radio)
{
MobilityHelper mobility;
mobility.SetPositionAllocator ("ns3::UniformDiscPositionAllocator",
"rho", DoubleValue (radio),
"X", DoubleValue (0.0),
"Y", DoubleValue (0.0),
"Z", DoubleValue (0.0));

mobility.SetMobilityModel ("ns3::ConstantPositionMobilityModel");
return mobility;
}



Del contenedor de nodos, creamos un centro de la estrella donde posicionaremos el primer nodo, que de ahora en adelante representará la pasarela:

MobilityHelper gatewayposition = createstarcenter();
gatewayposition.Install (nodes.Get(0));

Una vez creados los dispositivos y emplazados (la pasarela en la posición x,y,z a 0,0,0 y el resto de forma aleatoria), vamos a seguir configurando nuestros nodos, configurando una interfaz wifi en cada uno de los nodos. La tecnología inalámbrica WIFI está de sobra probada en NS3 y nos sirve de punto de partida para nuestro ejemplo.
Para la creación de NetDevice por cada Nodo e instalarlo en cada uno de los nodos, usaremos los asistentes, nos crearemos un asistente para la capa física (wifiPhy), la capa Mac (wifiMac) y el objeto que modela el canal inalámbrico (wifiChannel) y al cual deberemos conectar todos los objetos que modelan la capa física.

WifiHelper wifi;
YansWifiPhyHelper wifiPhy = YansWifiPhyHelper::Default ();
YansWifiChannelHelper wifiChannel = YansWifiChannelHelper::Default ();
WifiMacHelper wifiMac;
wifiMac.SetType ("ns3::AdhocWifiMac");
wifiPhy.SetChannel(wifiChannel.Create());
NetDeviceContainer devices = wifi.Install (wifiPhy, wifiMac, nodes);

En este caso para el modelado de la capa física y el canal utilizamos el modelo Yans. Es el modelo por defecto que se utiliza pero, obviamente, el El módulo wifi es uno de los más completos y estudiados con varios modelos disponibles.
El tipo de MAC es ns3::AdhocWifiMac que modela relaciones punto a punto entre dos dispositivos con Wifi. A continuación establecemos el canal en el asistente wifiPhy para que todos los objetos creados de la capa física compartan el canal y se puedan comunicar entre ellos. Estas configuraciones, en la parte wifi, son las configuraciones por defecto. Por último, instalamos usando el asistente general del wifi la capa física y la MAC en todos los nodos. El asistente de wifi nos devuelve un contenedor con los NetDevices creados (y asociados a los nodos):

NetDeviceContainer devices = wifi.Install (wifiPhy, wifiMac, nodes);

Hemos usado la configuración por defecto, este es uno de los puntos más importantes en cuanto a una simulación, tienes que definir claramente las métricas en las cuales estás interesados y modelar de forma simple el resto de elementos. Por ejemplo, si estás interesado en un protocolo de comunicaciones de la capa de aplicación y quieres validar su funcionalidad, la capa física/MAC que utilices debe ser modelada de la forma más simple posible.

Si por contra, estás interesado en modelar un nuevo algoritmo de control de flujo de 802.11ax, es la capa de aplicación la que debe modelar lo mínimo para generar el tráfico necesario para probar la funcionalidad.

Hasta aquí hemos creado el “Hardware” de nuestra simulación, vamos a instalar y configurar el resto de elementos.

Pasemos a la parte de configuración de la pila de protocolos, en este caso vamos a instalar y configurar una pila de protocolos TCP/IP, para ello creamos un asistente y lo instalamos en todos los nodos:

InternetStackHelper internet;
internet.Install (nodes);

Una vez instalada, configuramos la dirección de red y le asignamos direcciones IP a las interfaces de red instaladas en los nodos:

Ipv4AddressHelper ipv4;
ipv4.SetBase ("10.1.1.0", "255.255.255.0");
Ipv4InterfaceContainer i = ipv4.Assign (devices);

en este caso, usamos la dirección de red 10.1.1.0 con máscara 255.255.255.0 y se les asigna, de forma consecutiva, a cada uno de los NetDevices en el container “devices” creando un contenedor de interfaces ya configuradas.

El último paso de creación de nuestro escenario propiamente dicho es la creación de aplicaciones que creen tráfico. Vamos a instalar un servidor UDP en la pasarela (nodo 0) y lo configuramos para que escuche en el puerto 2000 :

uint16_t port = 2000;
UdpServerHelper server(port);
ApplicationContainer gatewayapps = server.Install(nodes.Get(0));
gatewayapps.Start(Seconds(1.0));
gatewayapps.Stop(Seconds(11.0));

Como vemos en el código de arriba, creamos un asistente de servidor en el puerto deseado y lo instalamos en el nodo 0. Por último indicamos los momentos en que arrancamos y paramos el servidor en este caso sobre el contenedor que se ha creado. Un paso similar hay que hacer en los clientes, debemos crear clientes software configurados para enviar una cantidad de tráfico al servidor que acabamos de instalar en el todo 0 (y que obtenemos con nodes.Get(0)).
Obtenemos la dirección IP del servidor (la 0) del contenedor de interfaces, establecemos el tamaño en 32 bytes, que para la información generada por un sensor es suficiente. En principio generaremos como mucho 10 paquetes:

Address gatewayAddress = Address(i.GetAddress (0));
uint32_t packetSize = 32;
uint32_t maxPacketCount = 10;

Con esta configuración, se crea un asistente de cliente, se establece el intervalo de envío y se configura el cliente:

UdpClientHelper client (gatewayAddress, port);
Time interPacketInterval = Seconds (0.5);
client.SetAttribute("PacketSize", UintegerValue(packetSize));
client.SetAttribute ("Interval", TimeValue (interPacketInterval));
client.SetAttribute ("MaxPackets", UintegerValue(maxPacketCount));

Hay que resaltar cómo se configura, mediante el método SetAttribute, cada una de las características, indicando su nombre y valor, este mecanismo es muy utilizado por los objetos NS3 para la configuración de parámetros.
Por último, instalamos en los nodos un cliente (incluyendo la pasarela que actuará como sensor/servidor) creándose otro contenedor de aplicaciones que debemos arrancar y parar. Hay que asegurarse que el servidor esté arrancado antes que los clientes, por eso arrancamos en el segundo 2 y paramos mas tarde para no perder información.

ApplicationContainer apps = client.Install(nodes);
apps.Start(Seconds(2.0));
apps.Stop(Seconds(10.0));

Por último, arrancamos el simulador:

Simulator::Stop (Hours (24));
Simulator::Run ();
Simulator::Destroy ();

Los detalles del simulador lo veremos en otra entrada.

Con esto terminamos nuestra simulación, colocando el archivo en el directorio scratch lo ejecutamos:

/ns-3.30.1$ ./waf --run basic-iot-sensors
Waf: Entering directory `ns-allinone-3.30.1/ns-3.30.1/build'
Waf: Leaving directory `/ns-allinone-3.30.1/ns-3.30.1/build'
Build commands will be stored in build/compile_commands.json
'build' finished successfully (1.747s)

Que compila y ejecuta nuestra simulación. Realmente no vemos ningún flujo de información por lo que no sabemos si todo ha ido bien, en próximas entradas veremos cómo obtener información, mediante log y trazas, de qué está pasando en nuestra simulación.